/*******************************************************************************
 * Copyright (c) 2010, 2017 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.internal.core.project;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.core.project.IBundleClasspathEntry;
import org.eclipse.pde.core.project.IBundleProjectDescription;
import org.eclipse.pde.core.project.IBundleProjectService;
import org.eclipse.pde.core.project.IHostDescription;
import org.eclipse.pde.core.project.IPackageExportDescription;
import org.eclipse.pde.core.project.IPackageImportDescription;
import org.eclipse.pde.core.project.IRequiredBundleDescription;
import org.eclipse.pde.internal.build.Utils;
import org.eclipse.pde.internal.core.ICoreConstants;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.TargetPlatformHelper;
import org.eclipse.pde.internal.core.build.WorkspaceBuildModel;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;

/**
 * A bundle project description contains the meta-data required to define
 * a bundle or plug-in project.
 * <p>
 * Clients may instantiate this class.
 * </p>
 * @since 3.6
 */
public class BundleProjectDescription implements IBundleProjectDescription {

	private final IProject fProject;
	private IPath fRoot;
	private String fSymbolicName;
	private String fBundleName;
	private String fBundleVendor;
	private IHostDescription fHost;
	private URI fUri;
	private Version fVersion;
	private boolean fSingleton = false;
	private IPath fLocalization = null;
	private IPath fDefaultOuputFolder = null;
	private String[] fEEs;
	private String[] fNatures;
	private IBundleClasspathEntry[] fBundleClasspath = null;
	private String fActivator = null;
	private String fTargetVersion;
	private boolean fIsEquinox = false;
	private boolean fIsExtensionRegistry = false;
	private String fActivationPolicy;
	private IRequiredBundleDescription[] fRequiredBundles;
	private IPackageImportDescription[] fImports;
	private IPackageExportDescription[] fExports;
	private IPath[] fBinIncludes;
	private IBundleProjectService fService;
	private String[] fLaunchShortcuts;
	private String fExportWizard;
	private final Map<String, String> fHeaders = new HashMap<>();
	private Map<String, String> fReadHeaders = null;

	/**
	 * Constructs a bundle description for the specified project.
	 *
	 * @param project project that may or may not exist
	 */
	public BundleProjectDescription(IProject project) throws CoreException {
		fProject = project;
		if (project.exists() && project.isOpen()) {
			initialize(project);
		}
	}

	/**
	 * Returns the bundle project service.
	 *
	 * @return bundle project service
	 */
	IBundleProjectService getBundleProjectService() {
		if (fService == null) {
			fService = PDECore.getDefault().acquireService(IBundleProjectService.class);
		}
		return fService;
	}

	/**
	 * Returns the build model for the given project or <code>null</code>
	 * if none.
	 *
	 * @param project project
	 * @return build model or <code>null</code>
	 */
	private IBuild getBuildModel(IProject project) {
		IFile buildFile = PDEProject.getBuildProperties(project);
		if (buildFile.exists()) {
			WorkspaceBuildModel buildModel = new WorkspaceBuildModel(buildFile);
			buildModel.load();
			return buildModel.getBuild();
		}
		return null;
	}

	/**
	 * Returns the header value from the Map of ManifestElement's or <code>null</code> if none.
	 *
	 * @param headers map of ManifestElement's
	 * @param key header name
	 * @return header value or <code>null</code>
	 */
	private String getHeaderValue(Map<String, String> headers, String key) throws CoreException {
		ManifestElement[] elements = parseHeader(headers, key);
		if (elements != null) {
			if (elements.length > 0) {
				return elements[0].getValue();
			}
		}
		return null;
	}

	/**
	 * Parses the specified header.
	 *
	 * @return elements or <code>null</code> if none
	 */
	private ManifestElement[] parseHeader(Map<String, String> headers, String key) throws CoreException {
		String value = headers.get(key);
		if (value != null) {
			if (value.trim().length() > 0) {
				try {
					return ManifestElement.parseHeader(key, value);
				} catch (BundleException e) {
					throw new CoreException(Status.error(e.getMessage(), e));
				}
			}
			// empty header
			return new ManifestElement[0];
		}
		return null;
	}

	/**
	 * Initialize settings from the given project.
	 *
	 * @param project project
	 * @exception CoreException if unable to initialize
	 */
	private void initialize(IProject project) throws CoreException {
		IContainer root = PDEProject.getBundleRoot(project);
		if (root != project) {
			setBundleRoot(root.getProjectRelativePath());
		}
		IEclipsePreferences node = new ProjectScope(project).getNode(PDECore.PLUGIN_ID);
		if (node != null) {
			setExtensionRegistry(node.getBoolean(ICoreConstants.EXTENSIONS_PROPERTY, true));
			setEquinox(node.getBoolean(ICoreConstants.EQUINOX_PROPERTY, true));
		}
		// export wizard and launch shortcuts
		setExportWizardId(PDEProject.getExportWizard(project));
		setLaunchShortcuts(PDEProject.getLaunchShortcuts(project));
		// location and natures
		setLocationURI(project.getDescription().getLocationURI());
		setNatureIds(project.getDescription().getNatureIds());

		IFile manifest = PDEProject.getManifest(project);
		if (manifest.exists()) {
			Map<String, String> headers;
			try {
				headers = ManifestElement.parseBundleManifest(manifest.getContents(), null);
				fReadHeaders = headers;
			} catch (IOException | BundleException e) {
				throw new CoreException(Status.error(e.getMessage(), e));
			}
			setActivator(getHeaderValue(headers, Constants.BUNDLE_ACTIVATOR));
			setBundleName(getHeaderValue(headers, Constants.BUNDLE_NAME));
			setBundleVendor(getHeaderValue(headers, Constants.BUNDLE_VENDOR));
			String version = getHeaderValue(headers, Constants.BUNDLE_VERSION);
			if (version != null) {
				setBundleVersion(new Version(version));
			}
			IJavaProject jp = JavaCore.create(project);
			if (jp.exists()) {
				setDefaultOutputFolder(jp.getOutputLocation().removeFirstSegments(1));
			}
			ManifestElement[] elements = parseHeader(headers, Constants.FRAGMENT_HOST);
			if (elements != null && elements.length > 0) {
				setHost(getBundleProjectService().newHost(elements[0].getValue(), getRange(elements[0].getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE))));
			}
			String value = getHeaderValue(headers, Constants.BUNDLE_LOCALIZATION);
			if (value != null) {
				setLocalization(IPath.fromOSString(value));
			}
			elements = parseHeader(headers, Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT);
			if (elements != null && elements.length > 0) {
				String[] keys = new String[elements.length];
				for (int i = 0; i < elements.length; i++) {
					keys[i] = elements[i].getValue();
				}
				setExecutionEnvironments(keys);
			}
			IBuild build = getBuildModel(project);
			elements = parseHeader(headers, Constants.BUNDLE_CLASSPATH);
			IBundleClasspathEntry[] classpath = null;
			if (elements != null && elements.length > 0) {
				List<IBundleClasspathEntry> collect = new ArrayList<>();
				for (ManifestElement element : elements) {
					String libName = element.getValue();
					IBundleClasspathEntry[] entries = getClasspathEntries(project, build, libName);
					if (entries != null) {
						Collections.addAll(collect, entries);
					}
				}
				classpath = collect.toArray(new IBundleClasspathEntry[collect.size()]);
			} else if (elements == null) {
				// default bundle classpath of '.'
				classpath = getClasspathEntries(project, build, "."); //$NON-NLS-1$
			}
			setBundleClasspath(classpath);
			elements = parseHeader(headers, Constants.BUNDLE_SYMBOLICNAME);
			if (elements != null && elements.length > 0) {
				setSymbolicName(elements[0].getValue());
				String directive = elements[0].getDirective(Constants.SINGLETON_DIRECTIVE);
				if (directive == null) {
					directive = elements[0].getAttribute(Constants.SINGLETON_DIRECTIVE);
				}
				setSingleton("true".equals(directive)); //$NON-NLS-1$
			}
			elements = parseHeader(headers, Constants.IMPORT_PACKAGE);
			if (elements != null) {
				if (elements.length > 0) {
					IPackageImportDescription[] imports = new IPackageImportDescription[elements.length];
					for (int i = 0; i < elements.length; i++) {
						boolean optional = Constants.RESOLUTION_OPTIONAL.equals(elements[i].getDirective(Constants.RESOLUTION_DIRECTIVE)) || "true".equals(elements[i].getAttribute(ICoreConstants.OPTIONAL_ATTRIBUTE)); //$NON-NLS-1$
						String pv = elements[i].getAttribute(ICoreConstants.PACKAGE_SPECIFICATION_VERSION);
						if (pv == null) {
							pv = elements[i].getAttribute(Constants.VERSION_ATTRIBUTE);
						}
						imports[i] = getBundleProjectService().newPackageImport(elements[i].getValue(), getRange(pv), optional);
					}
					setPackageImports(imports);
				} else {
					// empty header - should be maintained
					setHeader(Constants.IMPORT_PACKAGE, ""); //$NON-NLS-1$
				}
			}
			elements = parseHeader(headers, Constants.EXPORT_PACKAGE);
			if (elements != null && elements.length > 0) {
				IPackageExportDescription[] exports = new IPackageExportDescription[elements.length];
				for (int i = 0; i < elements.length; i++) {
					ManifestElement exp = elements[i];
					String pv = exp.getAttribute(ICoreConstants.PACKAGE_SPECIFICATION_VERSION);
					if (pv == null) {
						pv = exp.getAttribute(Constants.VERSION_ATTRIBUTE);
					}
					String directive = exp.getDirective(ICoreConstants.FRIENDS_DIRECTIVE);
					boolean internal = "true".equals(exp.getDirective(ICoreConstants.INTERNAL_DIRECTIVE)) || directive != null; //$NON-NLS-1$
					String[] friends = null;
					if (directive != null) {
						friends = ManifestElement.getArrayFromList(directive);
					}
					exports[i] = getBundleProjectService().newPackageExport(exp.getValue(), getVersion(pv), !internal,
							friends != null ? List.of(friends) : List.of());
				}
				setPackageExports(exports);
			}
			elements = parseHeader(headers, Constants.REQUIRE_BUNDLE);
			if (elements != null && elements.length > 0) {
				IRequiredBundleDescription[] req = new IRequiredBundleDescription[elements.length];
				for (int i = 0; i < elements.length; i++) {
					ManifestElement rb = elements[i];
					boolean reexport = Constants.VISIBILITY_REEXPORT.equals(rb.getDirective(Constants.VISIBILITY_DIRECTIVE)) || "true".equals(rb.getAttribute(ICoreConstants.REPROVIDE_ATTRIBUTE)); //$NON-NLS-1$
					boolean optional = Constants.RESOLUTION_OPTIONAL.equals(rb.getDirective(Constants.RESOLUTION_DIRECTIVE)) || "true".equals(rb.getAttribute(ICoreConstants.OPTIONAL_ATTRIBUTE)); //$NON-NLS-1$
					req[i] = getBundleProjectService().newRequiredBundle(rb.getValue(), getRange(rb.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE)), optional, reexport);
				}
				setRequiredBundles(req);
			}
			String lazy = getHeaderValue(headers, ICoreConstants.ECLIPSE_AUTOSTART);
			if (lazy == null) {
				lazy = getHeaderValue(headers, ICoreConstants.ECLIPSE_LAZYSTART);
				if (lazy == null) {
					setActivationPolicy(getHeaderValue(headers, Constants.BUNDLE_ACTIVATIONPOLICY));
				}
			}
			if ("true".equals(lazy)) { //$NON-NLS-1$
				setActivationPolicy(Constants.ACTIVATION_LAZY);
			}
			String latest = TargetPlatformHelper.getTargetVersionString();
			IPluginModelBase model = PluginRegistry.findModel(project);
			if (model != null) {
				IPluginBase base = model.getPluginBase();
				String tv = TargetPlatformHelper.getTargetVersionForSchemaVersion(base.getSchemaVersion());
				if (!tv.equals(latest)) {
					setTargetVersion(tv);
				}
			}
			if (build != null) {
				IBuildEntry entry = build.getEntry(IBuildEntry.BIN_INCLUDES);
				if (entry != null) {
					String[] tokens = entry.getTokens();
					if (tokens != null && tokens.length > 0) {
						List<String> strings = new ArrayList<>();
						Collections.addAll(strings, tokens);
						// remove the default entries
						strings.remove("META-INF/"); //$NON-NLS-1$
						String[] names = ProjectModifyOperation.getLibraryNames(this);
						if (names != null) {
							for (String name : names) {
								strings.remove(name);
								// if the library is a folder, account for trailing slash - see bug 306991
								IPath path = IPath.fromOSString(name);
								if (path.getFileExtension() == null) {
									strings.remove(name + "/"); //$NON-NLS-1$
								}
							}
						}
						// set left overs
						if (!strings.isEmpty()) {
							IPath[] paths = new IPath[strings.size()];
							for (int i = 0; i < strings.size(); i++) {
								paths[i] = IPath.fromOSString(strings.get(i));
							}
							setBinIncludes(paths);
						}
					}
				}
			}
		}
	}

	/**
	 * Returns bundle classpath specifications associated with the specified library or <code>null</code>.
	 *
	 * @param project associated project
	 * @param build build properties object or <code>null</code>
	 * @param libraryName name of library classpath is for
	 * @return bundle classpath specifications associated with the specified library or <code>null</code>
	 * @exception CoreException if unable to access associated Java project for source entries
	 */
	private IBundleClasspathEntry[] getClasspathEntries(IProject project, IBuild build, String libraryName) throws CoreException {
		if (build != null) {
			IBuildEntry entry = build.getEntry(IBuildEntry.JAR_PREFIX + libraryName);
			if (entry == null) {
				entry = build.getEntry(IBuildEntry.OUTPUT_PREFIX + libraryName);
				if (entry == null) {
					// no source or class file folder
					return new IBundleClasspathEntry[] {getBundleProjectService().newBundleClasspathEntry(null, null, IPath.fromOSString(libraryName))};
				}
				// base the entries on class file folders
				return getClasspathEntries(project, entry, true);
			}
			// base the entries on source folders
			return getClasspathEntries(project, entry, false);
		}
		return null;
	}

	/**
	 * Creates and returns a bundle claspath specifications for the given source.<library> build
	 * entry
	 *
	 * @param binary whether a binary folder (<code>true</code>) or source folder (<code>false</code>)
	 * @return associated bundle classpath specifications or <code>null</code> if a malformed entry
	 * @throws CoreException if unable to access Java build path
	 */
	private IBundleClasspathEntry[] getClasspathEntries(IProject project, IBuildEntry entry, boolean binary) throws CoreException {
		String[] tokens = entry.getTokens();
		IPath lib = null;
		if (binary) {
			lib = IPath.fromOSString(entry.getName().substring(IBuildEntry.OUTPUT_PREFIX.length()));
		} else {
			lib = IPath.fromOSString(entry.getName().substring(IBuildEntry.JAR_PREFIX.length()));
		}
		if (tokens != null && tokens.length > 0) {
			IBundleClasspathEntry[] bces = new IBundleClasspathEntry[tokens.length];
			for (int i = 0; i < tokens.length; i++) {
				IPath path = IPath.fromOSString(tokens[i]);
				IBundleClasspathEntry spec = null;
				if (binary) {
					spec = getBundleProjectService().newBundleClasspathEntry(null, path, lib);
				} else {
					IJavaProject jp = JavaCore.create(project);
					IPath output = null;
					if (jp.exists()) {
						IClasspathEntry[] rawClasspath = jp.getRawClasspath();
						for (IClasspathEntry cpe : rawClasspath) {
							if (cpe.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
								if (cpe.getPath().removeFirstSegments(1).equals(path)) {
									output = cpe.getOutputLocation();
									if (output != null) {
										output = output.removeFirstSegments(1);
									}
									break;
								}
							}
						}
					}
					spec = getBundleProjectService().newBundleClasspathEntry(path, output, lib);
				}
				bces[i] = spec;
			}
			return bces;
		}
		return null;
	}

	/**
	 * Create and return a version range from the given string or <code>null</code>.
	 *
	 * @param version version range string or <code>null</code>
	 * @return version range or <code>null</code>
	 */
	private VersionRange getRange(String version) {
		if (version != null) {
			return Utils.parseVersionRange(version);
		}
		return null;
	}

	/**
	 * Creates and returns a version from the given string or <code>null</code>.
	 *
	 * @param version version string or <code>null</code>
	 * @return version or <code>null</code>
	 */
	private Version getVersion(String version) {
		if (version != null) {
			return new Version(version);
		}
		return null;
	}

	@Override
	public void apply(IProgressMonitor monitor) throws CoreException {
		ProjectModifyOperation operation = new ProjectModifyOperation();
		operation.execute(monitor, this);
		fService = null;
	}

	@Override
	public void setSymbolicName(String name) {
		fSymbolicName = name;
	}

	@Override
	public String getSymbolicName() {
		return fSymbolicName;
	}

	@Override
	public void setLocationURI(URI location) {
		fUri = location;
	}

	@Override
	public URI getLocationURI() {
		return fUri;
	}

	@Override
	public void setBundleName(String name) {
		fBundleName = name;
	}

	@Override
	public String getBundleName() {
		return fBundleName;
	}

	@Override
	public void setBundleVendor(String name) {
		fBundleVendor = name;
	}

	@Override
	public String getBundleVendor() {
		return fBundleVendor;
	}

	@Override
	public void setBundleVersion(Version version) {
		fVersion = version;
	}

	@Override
	public Version getBundleVersion() {
		return fVersion;
	}

	@Override
	public void setSingleton(boolean singleton) {
		fSingleton = singleton;
	}

	@Override
	public boolean isSingleton() {
		return fSingleton;
	}

	@Override
	public void setLocalization(IPath path) {
		fLocalization = path;
	}

	@Override
	public IPath getLocalization() {
		return fLocalization;
	}

	@Override
	public String[] getNatureIds() {
		if (fNatures == null) {
			return new String[0];
		}
		String[] copy = new String[fNatures.length];
		System.arraycopy(fNatures, 0, copy, 0, fNatures.length);
		return copy;
	}

	@Override
	public void setNatureIds(String[] natures) {
		String[] copy = null;
		if (natures != null) {
			copy = new String[natures.length];
			System.arraycopy(natures, 0, copy, 0, natures.length);
		}
		fNatures = copy;
	}

	@Override
	public boolean hasNature(String natureId) {
		if (fNatures != null) {
			for (String nature : fNatures) {
				if (nature.equals(natureId)) {
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public void setHost(IHostDescription host) {
		fHost = host;
	}

	@Override
	public IHostDescription getHost() {
		return fHost;
	}

	@Override
	public void setDefaultOutputFolder(IPath output) {
		fDefaultOuputFolder = output;
	}

	@Override
	public IPath getDefaultOutputFolder() {
		return fDefaultOuputFolder;
	}

	@Override
	public void setExecutionEnvironments(String[] environments) {
		fEEs = environments;
	}

	@Override
	public String[] getExecutionEnvironments() {
		return fEEs;
	}

	@Override
	public IBundleClasspathEntry[] getBundleClasspath() {
		return fBundleClasspath;
	}

	@Override
	public void setActivator(String className) {
		fActivator = className;
	}

	@Override
	public String getActivator() {
		return fActivator;
	}

	@Override
	public void setTargetVersion(String version) {
		fTargetVersion = version;
	}

	@Override
	public String getTargetVersion() {
		return fTargetVersion;
	}

	@Override
	public void setEquinox(boolean equinox) {
		fIsEquinox = equinox;
	}

	@Override
	public boolean isEquinox() {
		return fIsEquinox;
	}

	@Override
	public void setRequiredBundles(IRequiredBundleDescription[] bundles) {
		fRequiredBundles = bundles;
	}

	@Override
	public IRequiredBundleDescription[] getRequiredBundles() {
		return fRequiredBundles;
	}

	@Override
	public void setPackageImports(IPackageImportDescription[] imports) {
		fImports = imports;
	}

	@Override
	public IPackageImportDescription[] getPackageImports() {
		return fImports;
	}

	@Override
	public void setPackageExports(IPackageExportDescription[] exports) {
		fExports = exports;
	}

	@Override
	public IPackageExportDescription[] getPackageExports() {
		return fExports;
	}

	@Override
	public IProject getProject() {
		return fProject;
	}

	@Override
	@Deprecated
	public void setBundleClassath(IBundleClasspathEntry[] entries) {
		setBundleClasspath(entries);
	}

	@Override
	public void setBundleClasspath(IBundleClasspathEntry[] entries) {
		fBundleClasspath = entries;
	}

	@Override
	public void setBinIncludes(IPath[] paths) {
		fBinIncludes = paths;
	}

	@Override
	public IPath[] getBinIncludes() {
		return fBinIncludes;
	}

	@Override
	public void setBundleRoot(IPath path) {
		fRoot = path;
	}

	@Override
	public IPath getBundleRoot() {
		return fRoot;
	}

	@Override
	public boolean isExtensionRegistry() {
		return fIsExtensionRegistry;
	}

	@Override
	public void setExtensionRegistry(boolean supportExtensions) {
		fIsExtensionRegistry = supportExtensions;
	}

	@Override
	public String[] getLaunchShortcuts() {
		return fLaunchShortcuts;
	}

	@Override
	public void setLaunchShortcuts(String[] ids) {
		fLaunchShortcuts = ids;
	}

	@Override
	public String getExportWizardId() {
		return fExportWizard;
	}

	@Override
	public void setExportWizardId(String id) {
		fExportWizard = id;
	}

	@Override
	public void setActivationPolicy(String policy) {
		if (Constants.ACTIVATION_LAZY.equals(policy)) {
			fActivationPolicy = policy;
		} else {
			fActivationPolicy = null;
		}
	}

	@Override
	public String getActivationPolicy() {
		return fActivationPolicy;
	}

	@Override
	public void setHeader(String header, String value) {
		fHeaders.put(header, value);
	}

	@Override
	public String getHeader(String header) {
		if (fHeaders.containsKey(header)) { // might be null so check contains
			return fHeaders.get(header);
		}
		if (fReadHeaders != null) {
			if (fReadHeaders.containsKey(header)) {
				String value = fReadHeaders.get(header);
				if (value == null) {
					// Return the empty string for present empty headers (instead of null - which means missing)
					return ""; //$NON-NLS-1$
				}
				return value;
			}
		}
		return null;
	}

	/**
	 * Returns any extra headers that have been specified. A map of header names to header
	 * values.
	 *
	 * @return a map of header names to header values, possible empty
	 */
	Map<String, String> getExtraHeaders() {
		return fHeaders;
	}

}
