/*******************************************************************************
 * Copyright (c) 2008, 2018 Code 9 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:
 *   Code 9 - initial API and implementation
 *   IBM - ongoing development
 *   SAP AG - ongoing development
 ******************************************************************************/
package org.eclipse.equinox.p2.publisher.actions;

import java.io.*;
import java.net.URL;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.CollectionUtils;
import org.eclipse.equinox.internal.p2.metadata.ArtifactKey;
import org.eclipse.equinox.internal.p2.publisher.Activator;
import org.eclipse.equinox.internal.p2.publisher.Messages;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitDescription;
import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitFragmentDescription;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.metadata.VersionRange;
import org.eclipse.equinox.p2.publisher.*;
import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
import org.eclipse.equinox.spi.p2.publisher.PublisherHelper;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;

public class JREAction extends AbstractPublisherAction {
	private static final String DEFAULT_JRE_NAME = "a.jre"; //$NON-NLS-1$
	private static final Version DEFAULT_JRE_VERSION = Version.parseVersion("9.0"); //$NON-NLS-1$
	private static final String DEFAULT_PROFILE = "JavaSE-9"; //$NON-NLS-1$
	private static final String PROFILE_LOCATION = "jre.action.profile.location"; //$NON-NLS-1$
	private static final String PROFILE_NAME = "osgi.java.profile.name"; //$NON-NLS-1$
	private static final String PROFILE_TARGET_VERSION = "org.eclipse.jdt.core.compiler.codegen.targetPlatform"; //$NON-NLS-1$
	private static final String PROFILE_SYSTEM_PACKAGES = "org.osgi.framework.system.packages"; //$NON-NLS-1$

	public static final String NAMESPACE_OSGI_EE = "osgi.ee"; //$NON-NLS-1$
	public static final String VERSION_OSGI_EE = "version"; //$NON-NLS-1$

	private File jreLocation;
	private String environment;
	private Map<String, String> profileProperties;
	private MultiStatus resultStatus;

	public JREAction(File location) {
		this.jreLocation = location;
	}

	public JREAction(String environment) {
		this.environment = environment;
	}

	@Override public IStatus perform(IPublisherInfo publisherInfo, IPublisherResult results, IProgressMonitor monitor) {
		String problemMessage = NLS.bind(Messages.message_problemsWhilePublishingEE, jreLocation != null ? jreLocation : environment);
		resultStatus = new MultiStatus(Activator.ID, 0, problemMessage, null);

		initialize(publisherInfo);
		IArtifactDescriptor artifact = createJREData(results);
		if (artifact != null)
			publishArtifact(artifact, new File[] {jreLocation}, null, publisherInfo, createRootPrefixComputer(jreLocation));

		if (resultStatus.isOK())
			return Status.OK_STATUS;
		return resultStatus;
	}

	private static Status newErrorStatus(String message, Exception exception) {
		return new Status(IStatus.ERROR, Activator.ID, message, exception);
	}

	private static Status newWarningStatus(String message) {
		return new Status(IStatus.WARNING, Activator.ID, message, null);
	}

	/**
	 * Creates IUs and artifact descriptors for the JRE.  The resulting IUs are added
	 * to the given set, and the resulting artifact descriptor, if any, is returned.
	 * If the jreLocation is <code>null</code>, default information is generated.
	 */
	protected IArtifactDescriptor createJREData(IPublisherResult results) {
		InstallableUnitDescription iu = new MetadataFactory.InstallableUnitDescription();
		iu.setSingleton(false);
		iu.setId(DEFAULT_JRE_NAME);
		iu.setVersion(DEFAULT_JRE_VERSION);
		iu.setTouchpointType(PublisherHelper.TOUCHPOINT_NATIVE);

		generateJREIUData(iu);

		InstallableUnitFragmentDescription cu = new InstallableUnitFragmentDescription();
		String configId = "config." + iu.getId();//$NON-NLS-1$
		cu.setId(configId);
		cu.setVersion(iu.getVersion());
		VersionRange range = iu.getVersion() == Version.emptyVersion ? VersionRange.emptyRange : new VersionRange(iu.getVersion(), true, Version.MAX_VERSION, true);
		cu.setHost(new IRequirement[] {MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, iu.getId(), range, null, false, false)});
		cu.setProperty(InstallableUnitDescription.PROP_TYPE_FRAGMENT, Boolean.TRUE.toString());
		cu.setCapabilities(new IProvidedCapability[] {PublisherHelper.createSelfCapability(configId, iu.getVersion())});
		cu.setTouchpointType(PublisherHelper.TOUCHPOINT_NATIVE);
		Map<String, String> touchpointData = new HashMap<>();

		if (jreLocation == null || !jreLocation.isDirectory()) {
			touchpointData.put("install", ""); //$NON-NLS-1$ //$NON-NLS-2$
			cu.addTouchpointData(MetadataFactory.createTouchpointData(touchpointData));
			results.addIU(MetadataFactory.createInstallableUnit(iu), IPublisherResult.ROOT);
			results.addIU(MetadataFactory.createInstallableUnit(cu), IPublisherResult.ROOT);
			return null;
		}

		//Generate artifact for JRE
		IArtifactKey key = new ArtifactKey(PublisherHelper.BINARY_ARTIFACT_CLASSIFIER, iu.getId(), iu.getVersion());
		iu.setArtifacts(new IArtifactKey[] {key});
		results.addIU(MetadataFactory.createInstallableUnit(iu), IPublisherResult.ROOT);

		//Create config info for the CU
		String configurationData = "unzip(source:@artifact, target:${installFolder});"; //$NON-NLS-1$
		touchpointData.put("install", configurationData); //$NON-NLS-1$
		String unConfigurationData = "cleanupzip(source:@artifact, target:${installFolder});"; //$NON-NLS-1$
		touchpointData.put("uninstall", unConfigurationData); //$NON-NLS-1$
		cu.addTouchpointData(MetadataFactory.createTouchpointData(touchpointData));
		results.addIU(MetadataFactory.createInstallableUnit(cu), IPublisherResult.ROOT);

		//Create the artifact descriptor
		return PublisherHelper.createArtifactDescriptor(info, key, jreLocation);
	}

	private List<IProvidedCapability> generateJRECapability(String id, Version version) {
		if (profileProperties == null)
			return Collections.emptyList();

		List<IProvidedCapability> result = new ArrayList<>();
		result.add(PublisherHelper.createSelfCapability(id, version));
		generateProvidedPackages(result);
		generateOsgiEESystemCapabilities(result);
		return result;
	}

	private void generateProvidedPackages(List<IProvidedCapability> result) {
		String packages = profileProperties.get(PROFILE_SYSTEM_PACKAGES);
		if (packages != null && (packages.trim().length() > 0)) {

			try {
				ManifestElement[] jrePackages = ManifestElement.parseHeader(PROFILE_SYSTEM_PACKAGES, packages);
				for (ManifestElement jrePackage : jrePackages) {
					String packageName = jrePackage.getValue();
					Version packageVersion = Version.create(jrePackage.getAttribute("version")); //$NON-NLS-1$
					result.add(MetadataFactory.createProvidedCapability(PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE, packageName, packageVersion));
				}
			} catch (BundleException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	void generateOsgiEESystemCapabilities(List<IProvidedCapability> result) {
		String message = NLS.bind(Messages.message_problemsWhileParsingProfileProperty, Constants.FRAMEWORK_SYSTEMCAPABILITIES);
		MultiStatus parsingStatus = new MultiStatus(Activator.ID, 0, message, null);

		String systemCapabilities = profileProperties.get(Constants.FRAMEWORK_SYSTEMCAPABILITIES);
		parseSystemCapabilities(systemCapabilities, parsingStatus, result);

		// result contains the valid entries, parsingStatus the invalid entries
		if (!parsingStatus.isOK())
			resultStatus.add(parsingStatus);
	}

	static void parseSystemCapabilities(String systemCapabilities, MultiStatus parsingStatus, List<IProvidedCapability> parsingResult) {
		if (systemCapabilities == null || (systemCapabilities.trim().length() == 0)) {
			return;
		}

		try {
			ManifestElement[] eeEntries = ManifestElement.parseHeader(Constants.FRAMEWORK_SYSTEMCAPABILITIES, systemCapabilities);
			parseSystemCapabilities(eeEntries, parsingStatus, parsingResult);

		} catch (BundleException e) {
			parsingStatus.add(newErrorStatus(e.getLocalizedMessage(), e));
		}
	}

	private static void parseSystemCapabilities(ManifestElement[] systemCapabilities, MultiStatus parsingStatus, List<IProvidedCapability> parsingResult) {
		for (ManifestElement systemCapability : systemCapabilities) {
			// this is general manifest syntax: a "manifest element" can have multiple "value components" -> all attributes apply to each value component (=namespace)
			String[] namespaces = systemCapability.getValueComponents();
			for (String namespace : namespaces) {
				if (NAMESPACE_OSGI_EE.equals(namespace)) { // this is the OSGi capability namespace "osgi.ee"
					parseEECapability(systemCapability, parsingStatus, parsingResult);

				} else {
					parsingStatus.add(newWarningStatus(NLS.bind(Messages.message_eeIgnoringNamespace, namespace)));
					continue;
				}
			}
		}
	}

	private static void parseEECapability(ManifestElement eeCapability, MultiStatus parsingStatus, List<IProvidedCapability> parsingResult) {
		String eeName = eeCapability.getAttribute(NAMESPACE_OSGI_EE); // this is an attribute required for capabilities in the "osgi.ee" namespace
		if (eeName == null) {
			parsingStatus.add(newErrorStatus(NLS.bind(Messages.message_eeMissingNameAttribute, eeCapability), null));
			return;
		}

		String[] eeVersions = parseEECapabilityVersion(eeCapability, parsingStatus);
		if (eeVersions == null) {
			// status was already updated by parse method
			return;
		}

		for (String rawVersion : eeVersions) {
			try {
				Version parsedVersion = Version.parseVersion(rawVersion);

				// complete record -> store
				Map<String, Object> capAttrs = new HashMap<>();
				capAttrs.put(NAMESPACE_OSGI_EE, eeName);
				capAttrs.put(VERSION_OSGI_EE, parsedVersion);

				parsingResult.add(MetadataFactory.createProvidedCapability(NAMESPACE_OSGI_EE, capAttrs));

			} catch (IllegalArgumentException e) {
				parsingStatus.add(newErrorStatus(NLS.bind(Messages.message_eeInvalidVersionAttribute, rawVersion), e));
			}
		}
	}

	private static String[] parseEECapabilityVersion(ManifestElement eeCapability, MultiStatus parsingStatus) {
		String singleVersion = eeCapability.getAttribute("version:Version"); //$NON-NLS-1$
		String[] multipleVersions = ManifestElement.getArrayFromList(eeCapability.getAttribute("version:List<Version>")); //$NON-NLS-1$

		if (singleVersion == null && multipleVersions == null) {
			parsingStatus.add(newErrorStatus(NLS.bind(Messages.message_eeMissingVersionAttribute, eeCapability), null));
			return null;

		} else if (singleVersion == null) {
			return multipleVersions;

		} else if (multipleVersions == null) {
			return new String[] {singleVersion};

		} else {
			parsingStatus.add(newErrorStatus(NLS.bind(Messages.message_eeDuplicateVersionAttribute, eeCapability), null));
			return null;
		}
	}

	private void generateJREIUData(InstallableUnitDescription iu) {
		if (profileProperties == null || profileProperties.size() == 0)
			return; //got nothing

		String profileLocation = profileProperties.get(PROFILE_LOCATION);

		String profileName = profileLocation != null ? new Path(profileLocation).lastSegment() : profileProperties.get(PROFILE_NAME);
		if (profileName.endsWith(".profile")) //$NON-NLS-1$
			profileName = profileName.substring(0, profileName.length() - 8);
		Version version = null;
		int idx = profileName.indexOf('-');
		if (idx != -1) {
			try {
				version = Version.parseVersion(profileName.substring(idx + 1));
			} catch (IllegalArgumentException e) {
				//ignore
			}
			profileName = profileName.substring(0, idx);
		}
		if (version == null) {
			try {
				String targetVersion = profileProperties.get(PROFILE_TARGET_VERSION);
				version = targetVersion != null ? Version.parseVersion(targetVersion) : null;
			} catch (IllegalArgumentException e) {
				//ignore
			}
		}

		if (version == null)
			version = DEFAULT_JRE_VERSION;

		iu.setVersion(version);

		profileName = profileName.replace('-', '.');
		profileName = profileName.replace('/', '.');
		profileName = profileName.replace('_', '.');
		iu.setId("a.jre." + profileName.toLowerCase()); //$NON-NLS-1$

		List<IProvidedCapability> capabilities = generateJRECapability(iu.getId(), iu.getVersion());
		iu.addProvidedCapabilities(capabilities);
	}

	private void initialize(IPublisherInfo publisherInfo) {
		this.info = publisherInfo;

		if (jreLocation != null) {

			File javaProfile = null;

			if (jreLocation.isDirectory()) {
				//Look for a JRE profile file to set version and capabilities
				File[] profiles = jreLocation.listFiles((FileFilter) pathname -> pathname.getAbsolutePath().endsWith(".profile")); //$NON-NLS-1$
				if (profiles != null && profiles.length > 0) {
					javaProfile = profiles[0];
				}
			} else if (jreLocation.isFile())
				javaProfile = jreLocation;
			else
				// jreLocation file does not exist
				throw new IllegalArgumentException(NLS.bind(Messages.exception_nonExistingJreLocationFile, jreLocation.getAbsolutePath()));

			profileProperties = loadProfile(javaProfile);
		}
		if (profileProperties == null) {
			String profileFile = (environment != null ? environment : DEFAULT_PROFILE).replace('/', '_') + ".profile"; //$NON-NLS-1$
			URL profileURL = getResouceFromSystemBundle(profileFile);
			profileProperties = loadProfile(profileURL);
		}
	}

	private static URL getResouceFromSystemBundle(String entry) {
		// get resource from system bundle which is typically on the Java boot classpath
		ClassLoader loader = FrameworkUtil.class.getClassLoader();
		return loader == null ? ClassLoader.getSystemResource(entry) : loader.getResource(entry);
	}

	private Map<String, String> loadProfile(File profileFile) {
		if (profileFile == null || !profileFile.exists())
			return null;

		try {
			InputStream stream = new BufferedInputStream(new FileInputStream(profileFile));
			Map<String, String> properties = loadProfile(stream);
			if (properties != null)
				properties.put(PROFILE_LOCATION, profileFile.getAbsolutePath());
			return properties;
		} catch (FileNotFoundException e) {
			//null
		}
		return null;
	}

	private Map<String, String> loadProfile(URL profileURL) {
		if (profileURL == null)
			return null;

		try {
			InputStream stream = profileURL.openStream();
			return loadProfile(stream);
		} catch (IOException e) {
			//null
		}
		return null;
	}

	/**
	 * Always closes the stream when done
	 */
	private Map<String, String> loadProfile(InputStream stream) {
		if (stream != null) {
			try {
				return CollectionUtils.loadProperties(stream);
			} catch (IOException e) {
				return null;
			} finally {
				try {
					stream.close();
				} catch (IOException e) {
					// error
				}
			}
		}
		return null;
	}
}
