/*******************************************************************************
 *  Copyright (c) 2007, 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
 *     Code 9 - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.installer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.installer.ui.SWTInstallAdvisor;
import org.eclipse.equinox.internal.provisional.p2.installer.InstallAdvisor;
import org.eclipse.equinox.internal.provisional.p2.installer.InstallDescription;
import org.eclipse.equinox.p2.core.*;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;

/**
 * This is a simple installer application built using P2.  The application must be given
 * an "install description" as a command line argument or system property 
 * ({@link #SYS_PROP_INSTALL_DESCRIPTION}).  The application reads this
 * install description, and looks for an existing profile in the local install registry that
 * matches it.  If no profile is found, it creates a new profile, and installs the root
 * IU in the install description into the profile. It may then launch the installed application,
 * depending on the specification in the install description.  If an existing profile is found,
 * the application instead performs an update on the existing profile with the new root
 * IU in the install description. Thus, an installed application can be updated by dropping
 * in a new install description file, and re-running this installer application.
 */
public class InstallApplication implements IApplication {
	/**
	 * A property whose value is the URL of an install description. An install description is a file
	 * that contains all the information required to complete the install.
	 */
	private static final String SYS_PROP_INSTALL_DESCRIPTION = "org.eclipse.equinox.p2.installDescription"; //$NON-NLS-1$

	/**
	 * The install advisor. This field is non null while the install application is running.
	 */
	private InstallAdvisor advisor;

	/**
	 * Throws an exception of severity error with the given error message.
	 */
	private static CoreException fail(String message, Throwable throwable) {
		return new CoreException(new Status(IStatus.ERROR, InstallerActivator.PI_INSTALLER, message, throwable));
	}

	/**
	 * Copied from ServiceHelper because we need to obtain services
	 * before p2 has been started.
	 */
	public static Object getService(BundleContext context, String name) {
		if (context == null)
			return null;
		ServiceReference<?> reference = context.getServiceReference(name);
		if (reference == null)
			return null;
		Object result = context.getService(reference);
		context.ungetService(reference);
		return result;
	}

	/**
	 * Loads the install description, filling in any missing data if needed.
	 */
	private InstallDescription computeInstallDescription() throws CoreException {
		InstallDescription description = fetchInstallDescription(SubMonitor.convert(null));
		return advisor.prepareInstallDescription(description);
	}

	private InstallAdvisor createInstallContext() {
		//TODO create an appropriate advisor depending on whether headless or GUI install is desired.
		InstallAdvisor result = new SWTInstallAdvisor();
		result.start();
		return result;
	}

	/**
	 * Fetch and return the install description to be installed.
	 */
	private InstallDescription fetchInstallDescription(SubMonitor monitor) throws CoreException {
		String site = System.getProperty(SYS_PROP_INSTALL_DESCRIPTION);
		try {
			return InstallDescriptionParser.createDescription(site, monitor);
		} catch (Exception e) {
			throw fail(Messages.App_InvalidSite + site, e);
		}
	}

	private IStatus getStatus(final Exception failure) {
		Throwable cause = failure;
		//unwrap target exception if applicable
		if (failure instanceof InvocationTargetException) {
			cause = ((InvocationTargetException) failure).getTargetException();
			if (cause == null)
				cause = failure;
		}
		if (cause instanceof CoreException)
			return ((CoreException) cause).getStatus();
		return new Status(IStatus.ERROR, InstallerActivator.PI_INSTALLER, Messages.App_Error, cause);
	}

	private void launchProduct(InstallDescription description) throws CoreException {
		IPath installLocation = description.getInstallLocation();
		IPath toRun = installLocation.append(description.getLauncherName());
		try {
			Runtime.getRuntime().exec(toRun.toString(), null, installLocation.toFile());
		} catch (IOException e) {
			throw fail(Messages.App_LaunchFailed + toRun, e);
		}
		//wait a few seconds to give the user a chance to read the message
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			//ignore
		}
	}

	@Override
	public Object start(IApplicationContext appContext) {
		try {
			appContext.applicationRunning();
			initializeProxySupport();
			advisor = createInstallContext();
			//fetch description of what to install
			InstallDescription description = null;
			try {
				description = computeInstallDescription();
				IProvisioningAgent agent = startAgent(description);
				//perform long running install operation
				InstallUpdateProductOperation operation = new InstallUpdateProductOperation(agent, description);
				IStatus result = advisor.performInstall(operation);
				if (!result.isOK()) {
					LogHelper.log(result);
					advisor.setResult(result);
					return IApplication.EXIT_OK;
				}
				//just exit after a successful update
				if (!operation.isFirstInstall())
					return IApplication.EXIT_OK;
				if (canAutoStart(description))
					launchProduct(description);
				else {
					//notify user that the product was installed
					//TODO present the user an option to immediately start the product
					advisor.setResult(result);
				}
				agent.stop();
			} catch (OperationCanceledException e) {
				advisor.setResult(Status.CANCEL_STATUS);
			} catch (Exception e) {
				IStatus error = getStatus(e);
				advisor.setResult(error);
				LogHelper.log(error);
			}
			return IApplication.EXIT_OK;
		} finally {
			if (advisor != null)
				advisor.stop();
		}
	}

	private void initializeProxySupport() {
		IProxyService proxies = (IProxyService) getService(InstallerActivator.getDefault().getContext(), IProxyService.class.getName());
		if (proxies == null)
			return;
		proxies.setProxiesEnabled(true);
		proxies.setSystemProxiesEnabled(true);
	}

	/**
	 * Returns whether the configuration described by the given install
	 * description can be started automatically.
	 */
	private boolean canAutoStart(InstallDescription description) {
		if (!description.isAutoStart())
			return false;
		//can't start if we don't know launcher name and path
		if (description.getLauncherName() == null || description.getInstallLocation() == null)
			return false;
		return advisor.promptForLaunch(description);
	}

	/**
	 * Starts the p2 bundles needed to continue with the install.
	 */
	private IProvisioningAgent startAgent(InstallDescription description) throws CoreException {
		IPath installLocation = description.getInstallLocation();
		if (installLocation == null)
			throw fail(Messages.App_NoInstallLocation, null);
		//set agent location if specified
		IPath agentLocation = description.getAgentLocation();
		try {
			IProvisioningAgentProvider provider = (IProvisioningAgentProvider) getService(InstallerActivator.getDefault().getContext(), IProvisioningAgentProvider.SERVICE_NAME);
			return provider.createAgent(agentLocation == null ? null : agentLocation.toFile().toURI());
		} catch (ProvisionException e) {
			throw fail(Messages.App_FailedStart, e);
		}
	}

	@Override
	public void stop() {
		//note this method can be called from another thread
		InstallAdvisor tempContext = advisor;
		if (tempContext != null) {
			tempContext.stop();
			advisor = null;
		}
	}
}
