/*******************************************************************************
 * Copyright (c) 2009, 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
 *     Sonatype, Inc. - ongoing development
 *     Rapicorp, Inc (Pascal Rapicault) - Bug 394156 - Add support for updates from one namespace to another
 ******************************************************************************/
package org.eclipse.equinox.p2.operations;

import java.util.Collection;
import java.util.Iterator;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.director.Explanation;
import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
import org.eclipse.equinox.internal.p2.operations.*;
import org.eclipse.equinox.internal.provisional.p2.director.PlannerStatus;
import org.eclipse.equinox.internal.provisional.p2.director.RequestStatus;
import org.eclipse.equinox.p2.engine.*;
import org.eclipse.equinox.p2.engine.query.UserVisibleRootQuery;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.planner.ProfileInclusionRules;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;

/**
 * An InstallOperation describes an operation that installs IInstallableUnits into
 * a profile.
 * 
 * The following snippet shows how one might use an InstallOperation to perform a synchronous resolution and
 * then kick off an install in the background:
 * 
 * <pre>
 * InstallOperation op = new InstallOperation(session, new IInstallableUnit [] { myIU });
 * IStatus result = op.resolveModal(monitor);
 * if (result.isOK()) {
 *   op.getProvisioningJob(monitor).schedule();
 * }
 * </pre>
 * 
 * @since 2.0
 * @see ProfileChangeOperation
 * @noextend This class is not intended to be subclassed by clients.
 */
public class InstallOperation extends ProfileChangeOperation {

	/**
	 * @since 2.1
	 */
	protected Collection<IInstallableUnit> toInstall;

	/**
	 * Create an install operation on the specified provisioning session that installs
	 * the supplied IInstallableUnits.  Unless otherwise specified, the operation will
	 * be associated with the currently running profile.
	 * 
	 * @param session the session to use for obtaining provisioning services
	 * @param toInstall the IInstallableUnits to be installed into the profile.
	 */
	public InstallOperation(ProvisioningSession session, Collection<IInstallableUnit> toInstall) {
		super(session);
		this.toInstall = toInstall;
	}

	@Override
	protected void computeProfileChangeRequest(MultiStatus status, IProgressMonitor monitor) {
		request = ProfileChangeRequest.createByProfileId(session.getProvisioningAgent(), profileId);
		IProfile profile = session.getProfileRegistry().getProfile(profileId);
		SubMonitor sub = SubMonitor.convert(monitor, Messages.InstallOperation_ComputeProfileChangeProgress, toInstall.size());
		for (IInstallableUnit entryToInstall : toInstall) {
			// If the user is installing a patch, we mark it optional.  This allows
			// the patched IU to be updated later by removing the patch.
			if (QueryUtil.isPatch(entryToInstall))
				request.setInstallableUnitInclusionRules(entryToInstall, ProfileInclusionRules.createOptionalInclusionRule(entryToInstall));

			// Check to see if it is already installed.  This may alter the request.
			IQueryResult<IInstallableUnit> alreadyInstalled = profile.query(QueryUtil.createIUQuery(entryToInstall.getId()), null);
			// TODO ideally we should only do this check if the iu is a singleton, but in practice many iu's that should
			// be singletons are not, so we don't check this (yet)
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=230878
			if (!alreadyInstalled.isEmpty()) { //  && installedIU.isSingleton()
				IInstallableUnit installedIU = alreadyInstalled.iterator().next();
				int compareTo = entryToInstall.getVersion().compareTo(installedIU.getVersion());
				// If the iu is a newer version of something already installed, consider this an update request
				if (compareTo > 0) {
					boolean lockedForUpdate = false;
					String value = profile.getInstallableUnitProperty(installedIU, IProfile.PROP_PROFILE_LOCKED_IU);
					if (value != null)
						lockedForUpdate = (Integer.parseInt(value) & IProfile.LOCK_UPDATE) == IProfile.LOCK_UPDATE;
					if (lockedForUpdate) {
						// Add a status telling the user that this implies an update, but the iu should not be updated
						status.merge(PlanAnalyzer.getStatus(IStatusCodes.ALTERED_IGNORED_IMPLIED_UPDATE, entryToInstall));
					} else {
						dealWithUpdates(status, profile, entryToInstall, installedIU);
					}
				} else if (compareTo < 0) {
					// An implied downgrade.  We will not put this in the plan, add a status informing the user
					status.merge(PlanAnalyzer.getStatus(IStatusCodes.ALTERED_IGNORED_IMPLIED_DOWNGRADE, entryToInstall));
				} else {
					if (UserVisibleRootQuery.isUserVisible(installedIU, profile))
						// It is already a root, nothing to do. We tell the user it was already installed
						status.merge(PlanAnalyzer.getStatus(IStatusCodes.ALTERED_IGNORED_ALREADY_INSTALLED, entryToInstall));
					else {
						// It was already installed but not as a root.  Tell the user that parts of it are already installed and mark
						// it as a root. 
						status.merge(PlanAnalyzer.getStatus(IStatusCodes.ALTERED_PARTIAL_INSTALL, entryToInstall));
						request.setInstallableUnitProfileProperty(entryToInstall, IProfile.PROP_PROFILE_ROOT_IU, Boolean.toString(true));
					}
				}
			} else {
				//Deal with the case of updates with renames
				boolean handled = false;
				Iterator<IInstallableUnit> allIUsIterator = profile.query(QueryUtil.ALL_UNITS, null).iterator();
				while (allIUsIterator.hasNext()) {
					IInstallableUnit iuAlreadyInstalled = allIUsIterator.next();
					if (entryToInstall.getUpdateDescriptor() != null && entryToInstall.getUpdateDescriptor().isUpdateOf(iuAlreadyInstalled)) {
						dealWithUpdates(status, profile, entryToInstall, iuAlreadyInstalled);
						handled = true;
						break;
					}
				}
				if (!handled)
					// Install it and mark as a root
					request.add(entryToInstall);
				request.setInstallableUnitProfileProperty(entryToInstall, IProfile.PROP_PROFILE_ROOT_IU, Boolean.toString(true));
			}
			sub.worked(1);
		}
		sub.done();
	}

	private void dealWithUpdates(MultiStatus status, IProfile profile, IInstallableUnit entryToInstall, IInstallableUnit installedIU) {
		request.add(entryToInstall);
		request.remove(installedIU);
		// Add a status informing the user that the update has been inferred
		status.merge(PlanAnalyzer.getStatus(IStatusCodes.ALTERED_IMPLIED_UPDATE, entryToInstall));
		// Mark it as a root if it hasn't been already
		if (!UserVisibleRootQuery.isUserVisible(installedIU, profile))
			request.setInstallableUnitProfileProperty(entryToInstall, IProfile.PROP_PROFILE_ROOT_IU, Boolean.toString(true));
	}

	@Override
	protected String getResolveJobName() {
		return Messages.InstallOperation_ResolveJobName;
	}

	@Override
	protected String getProvisioningJobName() {
		return Messages.InstallOperation_InstallJobName;

	}

	@Override
	ProvisioningContext getFirstPassProvisioningContext() {
		// Set it back to no referencing for first pass in case we reuse this context.
		context.setProperty(ProvisioningContext.FOLLOW_REPOSITORY_REFERENCES, null);
		return context;
	}

	@Override
	IFailedStatusEvaluator getSecondPassEvaluator() {
		return failedPlan -> {
			// Follow metadata repository references if the first try fails
			// There should be real API for this!
			if (missingRequirement(failedPlan))
				context.setProperty(ProvisioningContext.FOLLOW_REPOSITORY_REFERENCES, Boolean.toString(true));
			return context;
		};
	}

	// this is very reachy
	boolean missingRequirement(IProvisioningPlan failedPlan) {
		IStatus status = failedPlan.getStatus();
		RequestStatus requestStatus = null;
		if (status instanceof PlannerStatus)
			requestStatus = ((PlannerStatus) status).getRequestStatus();
		return requestStatus != null && requestStatus.getShortExplanation() == Explanation.MISSING_REQUIREMENT;
	}
}
