/*******************************************************************************
 * Copyright (c) 2013, 2018 Red Hat, Inc. 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:
 *     Red Hat, Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.p2.operations;

import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
import org.eclipse.equinox.internal.p2.operations.*;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.planner.IPlanner;
import org.eclipse.equinox.p2.planner.IProfileChangeRequest;

/**
 * <p>
 * <strong>EXPERIMENTAL</strong>. This class or interface has been added as
 * part of a work in progress. There is no guarantee that this API will
 * work or that it will remain the same. Please do not use this API without
 * consulting with the p2 team.
 * </p>
 * @noreference
 * @since 2.3
 */
public class RemediationOperation extends ProfileChangeOperation {

	private static int ZERO_WEIGHT = 0;
	private static int LOW_WEIGHT = 1;
	private static int MEDIUM_WEIGHT = 2;
	private static int HIGH_WEIGHT = 3;
	private List<Remedy> remedies;
	private Remedy bestSolutionChangingTheRequest;
	private Remedy bestSolutionChangingWhatIsInstalled;
	private Remedy currentRemedy;
	private RemedyConfig[] remedyConfigs;

	public RemedyConfig[] getRemedyConfigs() {
		return remedyConfigs;
	}

	public Remedy getCurrentRemedy() {
		return currentRemedy;
	}

	public void setCurrentRemedy(Remedy currentRemedy) {
		this.currentRemedy = currentRemedy;
		request = currentRemedy == null ? null : currentRemedy.getRequest();
	}

	private IProfileChangeRequest originalRequest;

	public RemediationOperation(ProvisioningSession session, IProfileChangeRequest iProfileChangeRequest) {
		this(session, iProfileChangeRequest, RemedyConfig.getAllRemedyConfigs());

	}

	public RemediationOperation(ProvisioningSession session, IProfileChangeRequest originalRequest, RemedyConfig[] remedyConfigs) {
		super(session);
		this.originalRequest = originalRequest;
		remedies = new ArrayList<>();
		this.remedyConfigs = remedyConfigs;
	}

	public Remedy bestSolutionChangingTheRequest() {
		return bestSolutionChangingTheRequest;
	}

	public Remedy bestSolutionChangingWhatIsInstalled() {
		return bestSolutionChangingWhatIsInstalled;
	}

	public List<Remedy> getRemedies() {
		return remedies;
	}

	@Override
	protected void computeProfileChangeRequest(MultiStatus status, IProgressMonitor monitor) {
		SubMonitor sub = SubMonitor.convert(monitor, 1);
		if (currentRemedy != null) {
			request = currentRemedy.getRequest();
			sub.worked(1);
			return;
		}
		try {
			status.add(computeAllRemediations(sub.newChild(1)));
		} catch (OperationCanceledException e) {
			status.add(Status.CANCEL_STATUS);
		}
		determineBestSolutions();
	}

	private IStatus computeAllRemediations(IProgressMonitor monitor) {
		SubMonitor sub = SubMonitor.convert(monitor, remedyConfigs.length);
		sub.setTaskName(Messages.RemediationOperation_ProfileChangeRequestProgress);
		List<Remedy> tmpRemedies = new ArrayList<>(remedyConfigs.length);
		try {
			for (int i = 0; i < remedyConfigs.length; i++) {
				sub.subTask((i + 1) + " / " + remedyConfigs.length); //$NON-NLS-1$
				if (sub.isCanceled())
					return Status.CANCEL_STATUS;
				Remedy remedy = computeRemedy(remedyConfigs[i], sub.newChild(1, SubMonitor.SUPPRESS_ALL_LABELS));
				if (remedy != null) {
					tmpRemedies.add(remedy);
				}
			}
		} finally {
			sub.done();
		}
		remedies = tmpRemedies;
		return getResolutionResult();
	}

	private void determineBestSolutions() {
		int beingInstalledWeight = 0;
		int installationWeight = 0;
		for (Remedy remedy : remedies) {
			if (remedy.getRequest() != null) {
				if (remedy.getBeingInstalledRelaxedWeight() > beingInstalledWeight && remedy.getInstallationRelaxedWeight() == 0) {
					bestSolutionChangingTheRequest = remedy;
					beingInstalledWeight = remedy.getBeingInstalledRelaxedWeight();
					continue;
				}
				if (remedy.getInstallationRelaxedWeight() > installationWeight && remedy.getBeingInstalledRelaxedWeight() == 0) {
					bestSolutionChangingWhatIsInstalled = remedy;
					installationWeight = remedy.getInstallationRelaxedWeight();
					continue;
				}
			}
		}
	}

	private Remedy computeRemedy(RemedyConfig configuration, IProgressMonitor monitor) {
		Remedy remedy = new Remedy(originalRequest);
		remedy.setConfig(configuration);
		IPlanner planner = session.getPlanner();
		RequestFlexer av = new RequestFlexer(planner);
		av.setAllowDifferentVersion(configuration.allowDifferentVersion);
		av.setAllowInstalledElementChange(configuration.allowInstalledUpdate);
		av.setAllowInstalledElementRemoval(configuration.allowInstalledRemoval);
		av.setAllowPartialInstall(configuration.allowPartialInstall);
		av.setProvisioningContext(getProvisioningContext());
		remedy.setRequest((ProfileChangeRequest) av.getChangeRequest(originalRequest, ((ProfileChangeRequest) originalRequest).getProfile(), monitor));
		if (remedy.getRequest() == null)
			return null;

		if (configuration.allowInstalledUpdate && !configuration.allowInstalledRemoval) {
			remedy.setInstallationRelaxedWeight(HIGH_WEIGHT);
		} else if (!configuration.allowInstalledUpdate && configuration.allowInstalledRemoval) {
			remedy.setInstallationRelaxedWeight(MEDIUM_WEIGHT);
		} else if (configuration.allowInstalledUpdate && configuration.allowInstalledRemoval) {
			remedy.setInstallationRelaxedWeight(LOW_WEIGHT);
		} else
			remedy.setInstallationRelaxedWeight(ZERO_WEIGHT);

		if (configuration.allowDifferentVersion && !configuration.allowPartialInstall) {
			remedy.setBeingInstalledRelaxedWeight(HIGH_WEIGHT);
		} else if (!configuration.allowDifferentVersion && configuration.allowPartialInstall) {
			remedy.setBeingInstalledRelaxedWeight(MEDIUM_WEIGHT);
		} else if (configuration.allowDifferentVersion && configuration.allowPartialInstall) {
			remedy.setBeingInstalledRelaxedWeight(LOW_WEIGHT);
		} else {
			remedy.setBeingInstalledRelaxedWeight(ZERO_WEIGHT);
		}
		computeRemedyDetails(remedy);
		return remedy;
	}

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

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

	@Override
	public ProvisioningJob getProvisioningJob(IProgressMonitor monitor) {
		IStatus status = getResolutionResult();
		if (status.getSeverity() != IStatus.CANCEL && status.getSeverity() != IStatus.ERROR) {
			if (job.getProvisioningPlan() != null) {
				ProfileModificationJob pJob = new ProfileModificationJob(getProvisioningJobName(), session, profileId, job.getProvisioningPlan(), job.getActualProvisioningContext());
				pJob.setAdditionalProgressMonitor(monitor);
				return pJob;
			}
		}
		return null;
	}

	public ProfileChangeRequest getOriginalRequest() {
		return (ProfileChangeRequest) originalRequest;
	}

	@Override
	void makeResolveJob(final IProgressMonitor monitor) {
		// throw away any previous requests
		request = null;
		noChangeRequest = PlanAnalyzer.getProfileChangeAlteredStatus();
		// the requestHolder is a hack to work around the fact that there is no public API
		// for the resolution job to get the request from the operation after it has been
		// computed.
		final ProfileChangeRequest[] requestHolder = new ProfileChangeRequest[1];
		job = new RemediationResolutionJob(getResolveJobName(), session, profileId, request, getFirstPassProvisioningContext(), getSecondPassEvaluator(), noChangeRequest, mon -> {
			SubMonitor sub = SubMonitor.convert(mon, 2);
			// We only check for other jobs running if this job is *not* scheduled
			if (job.getState() == Job.NONE && session.hasScheduledOperationsFor(profileId)) {
				noChangeRequest.add(PlanAnalyzer.getStatus(IStatusCodes.OPERATION_ALREADY_IN_PROGRESS, null));
			} else {
				sub.worked(1);
				computeProfileChangeRequest(noChangeRequest, sub.newChild(1));
				requestHolder[0] = RemediationOperation.this.request;
			}
		}, requestHolder, this);
	}

	@Override
	public IStatus getResolutionResult() {
		if (currentRemedy != null)
			return super.getResolutionResult();
		return remedies.size() > 0 ? Status.OK_STATUS : new Status(IStatus.ERROR, Constants.BUNDLE_ID, Messages.RemediationOperation_NoRemedyFound);
	}

	private void computeRemedyDetails(Remedy remedy) {
		ArrayList<String> updateIds = new ArrayList<>();
		for (IInstallableUnit addedIU : remedy.getRequest().getAdditions()) {
			for (IInstallableUnit removedIU : remedy.getRequest().getRemovals()) {
				if (removedIU.getId().equals(addedIU.getId())) {
					createModificationRemedyDetail(addedIU, removedIU, remedy);
					updateIds.add(addedIU.getId());
					break;
				}
			}
			if (!updateIds.contains(addedIU.getId())) {
				createAdditionRemedyDetail(addedIU, remedy);
			}
		}

		for (IInstallableUnit removedIU : remedy.getRequest().getRemovals()) {
			if (!updateIds.contains(removedIU.getId())) {
				createRemovalRemedyDetail(removedIU, remedy);
			}
		}

		for (IInstallableUnit addedIUinOriginalRequest : originalRequest.getAdditions()) {
			boolean found = false;
			for (IInstallableUnit addedIU : remedy.getRequest().getAdditions()) {
				if (addedIU.getId().equals(addedIUinOriginalRequest.getId())) {
					found = true;
					break;
				}
			}
			if (!found) {
				createNotAddedRemedyDetail(addedIUinOriginalRequest, remedy);
				found = false;
			}
		}
	}

	private void createNotAddedRemedyDetail(IInstallableUnit iu, Remedy remedy) {
		RemedyIUDetail iuDetail = new RemedyIUDetail(iu);
		iuDetail.setStatus(RemedyIUDetail.STATUS_NOT_ADDED);
		iuDetail.setRequestedVersion(iu.getVersion());
		remedy.addRemedyIUDetail(iuDetail);
	}

	private void createRemovalRemedyDetail(IInstallableUnit iu, Remedy remedy) {
		RemedyIUDetail iuDetail = new RemedyIUDetail(iu);
		iuDetail.setStatus(RemedyIUDetail.STATUS_REMOVED);
		iuDetail.setInstalledVersion(iu.getVersion());
		remedy.addRemedyIUDetail(iuDetail);
	}

	private void createAdditionRemedyDetail(IInstallableUnit iu, Remedy remedy) {
		RemedyIUDetail iuDetail = new RemedyIUDetail(iu);
		iuDetail.setStatus(RemedyIUDetail.STATUS_ADDED);
		iuDetail.setBeingInstalledVersion(iu.getVersion());
		iuDetail.setRequestedVersion(searchInOriginalRequest(iu.getId()));
		remedy.addRemedyIUDetail(iuDetail);
	}

	private void createModificationRemedyDetail(IInstallableUnit beingInstalledIU, IInstallableUnit installedIU, Remedy remedy) {
		RemedyIUDetail iuDetail = new RemedyIUDetail(beingInstalledIU);
		iuDetail.setStatus(RemedyIUDetail.STATUS_CHANGED);
		iuDetail.setBeingInstalledVersion(beingInstalledIU.getVersion());
		iuDetail.setInstalledVersion(installedIU.getVersion());
		iuDetail.setRequestedVersion(searchInOriginalRequest(beingInstalledIU.getId()));
		remedy.addRemedyIUDetail(iuDetail);
	}

	private Version searchInOriginalRequest(String id) {
		for (IInstallableUnit iu : originalRequest.getAdditions()) {
			if (iu.getId() == id)
				return iu.getVersion();
		}
		return null;
	}
}
