/*******************************************************************************
 * Copyright (c) 2007, 2022 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
 *   Daniel Le Berre - Fix in the encoding and the optimization function
 *   Alban Browaeys - Optimized string concatenation in bug 251357
 *   Jed Anderson - switch from opb files to API calls to DependencyHelper in bug 200380
 *   Sonatype, Inc. - ongoing development
 *   Rapicorp, Inc. - split the optimization function
 *   Red Hat, Inc. - support for remediation page
 *   Christoph Läubrich - access activator static singelton in a safe way
 ******************************************************************************/
package org.eclipse.equinox.internal.p2.director;

import java.util.*;
import java.util.Map.Entry;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.equinox.internal.p2.core.helpers.Tracing;
import org.eclipse.equinox.internal.p2.director.Explanation.NotInstallableRoot;
import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability;
import org.eclipse.equinox.internal.p2.metadata.InstallableUnit;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
import org.eclipse.equinox.p2.query.*;
import org.eclipse.osgi.util.NLS;
import org.sat4j.minisat.restarts.LubyRestarts;
import org.sat4j.pb.*;
import org.sat4j.pb.core.PBSolverResolution;
import org.sat4j.pb.tools.*;
import org.sat4j.specs.*;

/**
 * This class is the interface between SAT4J and the planner. It produces a
 * boolean satisfiability problem, invokes the solver, and converts the solver result
 * back into information understandable by the planner.
 */
public class Projector {
	/**
	 * The name of a Java system property specifying the timeout to set in the SAT solver.
	 * Note this value is not a time, but rather a conflict count. Essentially the solver
	 * will give up on a particular solution path when this number of conflicts is reached.
	 */
	private static final String PROP_PROJECTOR_TIMEOUT = "eclipse.p2.projector.timeout"; //$NON-NLS-1$
	/**
	 * The default SAT solver timeout (in number of conflicts). See bug 372529 for discussion.
	 */
	private static final int DEFAULT_SOLVER_TIMEOUT = 10000;
	static boolean DEBUG = Tracing.DEBUG_PLANNER_PROJECTOR;
	private static boolean DEBUG_ENCODING = Tracing.DEBUG_PLANNER_PROJECTOR_ENCODING;
	private IQueryable<IInstallableUnit> picker;
	private QueryableArray patches;

	private List<AbstractVariable> allOptionalAbstractRequirements;
	private List<AbstractVariable> abstractVariables;

	private Map<String, Map<Version, IInstallableUnit>> slice; //The IUs that have been considered to be part of the problem

	private IInstallableUnit selectionContext;

	DependencyHelper<Object, Explanation> dependencyHelper;
	private Collection<IInstallableUnit> solution;
	private Collection<Object> assumptions;

	private MultiStatus result;

	private Collection<IInstallableUnit> alreadyInstalledIUs;
	private IQueryable<IInstallableUnit> lastState;

	private boolean considerMetaRequirements;
	private IInstallableUnit entryPoint;
	private Map<IInstallableUnitFragment, Set<IInstallableUnit>> fragments = new HashMap<>();

	//Non greedy things
	private Set<IInstallableUnit> nonGreedyIUs; //All the IUs that would satisfy non greedy dependencies
	private Map<IInstallableUnit, AbstractVariable> nonGreedyVariables = new HashMap<>();
	private Map<AbstractVariable, List<Object>> nonGreedyProvider = new HashMap<>(); //Keeps track of all the "object" that provide an IU that is non greedly requested

	private boolean emptyBecauseFiltered;
	private boolean userDefinedFunction;

	static class AbstractVariable {
		//		private String name;

		public AbstractVariable(String name) {
			//						this.name = name;
		}

		public AbstractVariable() {
			// TODO Auto-generated constructor stub
		}

		@Override
		public String toString() {
			return "AbstractVariable: " + hashCode(); //$NON-NLS-1$
			//			return name == null ? "AbstractVariable: " + hashCode() : name; //$NON-NLS-1$
		}
	}

	/**
	 * Job for computing SAT failure explanation in the background.
	 */
	class ExplanationJob extends Job {
		private Set<Explanation> explanation;

		public ExplanationJob() {
			super(Messages.Planner_NoSolution);
			//explanations cannot be canceled directly, so don't show it to the user
			setSystem(true);
		}

		@Override
		public boolean belongsTo(Object family) {
			return family == ExplanationJob.this;
		}

		@Override
		protected void canceling() {
			super.canceling();
			dependencyHelper.stopExplanation();
		}

		public Set<Explanation> getExplanationResult() {
			return explanation;
		}

		@Override
		protected IStatus run(IProgressMonitor monitor) {
			long start = 0;
			if (DEBUG) {
				start = System.currentTimeMillis();
				Tracing.debug("Determining cause of failure: " + start); //$NON-NLS-1$
			}
			try {
				explanation = dependencyHelper.why();
				if (DEBUG) {
					long stop = System.currentTimeMillis();
					Tracing.debug("Explanation found: " + (stop - start)); //$NON-NLS-1$
					Tracing.debug("Explanation:"); //$NON-NLS-1$
					for (Explanation ex : explanation) {
						Tracing.debug(ex.toString());
					}
				}
			} catch (TimeoutException e) {
				if (DEBUG)
					Tracing.debug("Timeout while computing explanations"); //$NON-NLS-1$
			} finally {
				//must never have a null result, because caller is waiting on result to be non-null
				if (explanation == null)
					explanation = Collections.emptySet();
			}
			synchronized (this) {
				ExplanationJob.this.notify();
			}
			return Status.OK_STATUS;
		}

	}

	public Projector(IQueryable<IInstallableUnit> q, Map<String, String> context, Set<IInstallableUnit> nonGreedyIUs, boolean considerMetaRequirements) {
		picker = q;
		slice = new HashMap<>();
		selectionContext = InstallableUnit.contextIU(context);
		abstractVariables = new ArrayList<>();
		allOptionalAbstractRequirements = new ArrayList<>();
		result = new MultiStatus(DirectorActivator.PI_DIRECTOR, IStatus.OK, Messages.Planner_Problems_resolving_plan, null);
		assumptions = new ArrayList<>();
		this.nonGreedyIUs = nonGreedyIUs;
		this.considerMetaRequirements = considerMetaRequirements;
	}

	@SuppressWarnings("unchecked")
	public void encode(IInstallableUnit entryPointIU, IInstallableUnit[] alreadyExistingRoots, IQueryable<IInstallableUnit> installedIUs, Collection<IInstallableUnit> newRoots, IProgressMonitor monitor) {
		alreadyInstalledIUs = Arrays.asList(alreadyExistingRoots);
		lastState = installedIUs;
		this.entryPoint = entryPointIU;
		try {
			long start = 0;
			if (DEBUG) {
				start = System.currentTimeMillis();
				Tracing.debug("Start projection: " + start); //$NON-NLS-1$
			}
			IPBSolver solver;
			if (DEBUG_ENCODING) {
				solver = new UserFriendlyPBStringSolver<>();
			} else {
				if (userDefinedFunction) {
					PBSolverResolution mysolver = SolverFactory.newCompetPBResLongWLMixedConstraintsObjectiveExpSimp();
					mysolver.setSimplifier(mysolver.SIMPLE_SIMPLIFICATION);
					mysolver.setRestartStrategy(new LubyRestarts(512));
					solver = mysolver;
				} else {
					solver = SolverFactory.newEclipseP2();
				}
			}
			int timeout = DEFAULT_SOLVER_TIMEOUT;
			String timeoutString = null;
			try {
				// allow the user to specify a longer timeout.
				// only set the value if it is a positive integer larger than the default.
				// see https://bugs.eclipse.org/336967
				timeoutString = DirectorActivator.context.map(ctx -> ctx.getProperty(PROP_PROJECTOR_TIMEOUT))
						.orElse(null);
				if (timeoutString != null)
					timeout = Math.max(timeout, Integer.parseInt(timeoutString));
			} catch (Exception e) {
				// intentionally catch all errors (npe, number format, etc)
				// print out to syserr and fall through
				System.err.println("Ignoring user-specified 'eclipse.p2.projector.timeout' value of: " + timeoutString); //$NON-NLS-1$
				e.printStackTrace();
			}
			if (userDefinedFunction)
				solver.setTimeoutOnConflicts(timeout / 4);
			else
				solver.setTimeoutOnConflicts(timeout);

			IQueryResult<IInstallableUnit> queryResult = picker.query(QueryUtil.createIUAnyQuery(), null);
			if (DEBUG_ENCODING) {
				dependencyHelper = new LexicoHelper<>(solver, false);
				((UserFriendlyPBStringSolver<Object>) solver).setMapping(dependencyHelper.getMappingToDomain());
			} else {
				if (userDefinedFunction)
					dependencyHelper = new SteppedTimeoutLexicoHelper<>(solver);
				else
					dependencyHelper = new DependencyHelper<>(solver);
			}
			List<IInstallableUnit> iusToOrder = new ArrayList<>(queryResult.toSet());
			iusToOrder.sort(null);
			for (IInstallableUnit iu : iusToOrder) {
				if (monitor.isCanceled()) {
					result.merge(Status.CANCEL_STATUS);
					throw new OperationCanceledException();
				}
				if (iu != entryPointIU) {
					processIU(iu, false);
				}
			}
			createMustHave(entryPointIU, alreadyExistingRoots);

			createConstraintsForSingleton();

			createConstraintsForNonGreedy();

			createOptimizationFunction(entryPointIU, newRoots);
			if (DEBUG) {
				long stop = System.currentTimeMillis();
				Tracing.debug("Projection complete: " + (stop - start)); //$NON-NLS-1$
			}
			if (DEBUG_ENCODING) {
				System.out.println(solver.toString());
			}
		} catch (IllegalStateException e) {
			result.add(new Status(IStatus.ERROR, DirectorActivator.PI_DIRECTOR, e.getMessage(), e));
		} catch (ContradictionException e) {
			result.add(new Status(IStatus.ERROR, DirectorActivator.PI_DIRECTOR, Messages.Planner_Unsatisfiable_problem));
		}
	}

	private void createConstraintsForNonGreedy() throws ContradictionException {
		for (IInstallableUnit iu : nonGreedyIUs) {
			AbstractVariable var = getNonGreedyVariable(iu);
			List<Object> providers = nonGreedyProvider.get(var);
			if (providers == null || providers.size() == 0) {
				dependencyHelper.setFalse(var, new Explanation.MissingGreedyIU(iu));
			} else {
				createImplication(var, providers, Explanation.OPTIONAL_REQUIREMENT);//FIXME
			}
		}

	}

	private void createOptimizationFunction(IInstallableUnit entryPointIU, Collection<IInstallableUnit> newRoots) {
		if (!userDefinedFunction) {
			createStandardOptimizationFunction(entryPointIU, newRoots);
		} else {
			createUserDefinedOptimizationFunction(entryPointIU, newRoots);
		}
	}

	//Create an optimization function favoring the highest version of each IU
	private void createStandardOptimizationFunction(IInstallableUnit entryPointIU, Collection<IInstallableUnit> newRoots) {
		List<WeightedObject<? extends Object>> weights = new OptimizationFunction(lastState, abstractVariables, allOptionalAbstractRequirements, picker, selectionContext, slice).createOptimizationFunction(entryPointIU, newRoots);
		createObjectiveFunction(weights);
	}

	private void createUserDefinedOptimizationFunction(IInstallableUnit entryPointIU, Collection<IInstallableUnit> newRoots) {
		List<WeightedObject<? extends Object>> weights = new UserDefinedOptimizationFunction(lastState, abstractVariables, allOptionalAbstractRequirements, picker, selectionContext, slice, dependencyHelper, alreadyInstalledIUs).createOptimizationFunction(entryPointIU, newRoots);
		createObjectiveFunction(weights);
	}

	private void createObjectiveFunction(List<WeightedObject<? extends Object>> weightedObjects) {
		if (weightedObjects == null)
			return;
		if (DEBUG) {
			StringBuilder b = new StringBuilder();
			for (WeightedObject<? extends Object> object : weightedObjects) {
				if (b.length() > 0)
					b.append(", "); //$NON-NLS-1$
				b.append(object.getWeight());
				b.append(' ');
				b.append(object.thing);
			}
			Tracing.debug("objective function: " + b); //$NON-NLS-1$
		}
		@SuppressWarnings("unchecked")
		WeightedObject<Object>[] array = (WeightedObject<Object>[]) weightedObjects.toArray(new WeightedObject<?>[weightedObjects.size()]);
		dependencyHelper.setObjectiveFunction(array);
	}

	private void createMustHave(IInstallableUnit iu, IInstallableUnit[] alreadyExistingRoots) throws ContradictionException {
		processIU(iu, true);
		if (DEBUG) {
			Tracing.debug(iu + "=1"); //$NON-NLS-1$
		}
		// dependencyHelper.setTrue(variable, new Explanation.IUToInstall(iu));
		assumptions.add(iu);
	}

	private void createNegation(IInstallableUnit iu, IRequirement req) throws ContradictionException {
		if (DEBUG) {
			Tracing.debug(iu + "=0"); //$NON-NLS-1$
		}
		dependencyHelper.setFalse(iu, new Explanation.MissingIU(iu, req, iu == this.entryPoint));
	}

	// Check whether the requirement is applicable
	private boolean isApplicable(IRequirement req) {
		IMatchExpression<IInstallableUnit> filter = req.getFilter();
		return filter == null || filter.isMatch(selectionContext);
	}

	private boolean isApplicable(IInstallableUnit iu) {
		IMatchExpression<IInstallableUnit> filter = iu.getFilter();
		return filter == null || filter.isMatch(selectionContext);
	}

	private void expandNegatedRequirement(IRequirement req, IInstallableUnit iu, List<AbstractVariable> optionalAbstractRequirements, boolean isRootIu) throws ContradictionException {
		if (!isApplicable(req))
			return;
		List<IInstallableUnit> matches = getApplicableMatches(req);
		if (matches.isEmpty()) {
			return;
		}
		Explanation explanation;
		if (isRootIu) {
			IInstallableUnit reqIu = matches.get(0);
			if (alreadyInstalledIUs.contains(reqIu)) {
				explanation = new Explanation.IUInstalled(reqIu);
			} else {
				explanation = new Explanation.IUToInstall(reqIu);
			}
		} else {
			explanation = new Explanation.HardRequirement(iu, req);
		}
		createNegationImplication(iu, matches, explanation);
	}

	private void determinePotentialHostsForFragment(IInstallableUnit iu) {
		// determine matching hosts only for fragments
		if (!(iu instanceof IInstallableUnitFragment))
			return;

		IInstallableUnitFragment fragment = (IInstallableUnitFragment) iu;
		// for each host requirement, find matches and remember them
		for (IRequirement req : fragment.getHost()) {
			List<IInstallableUnit> matches = getApplicableMatches(req);
			rememberHostMatches((IInstallableUnitFragment) iu, matches);
		}
	}

	private void expandRequirement(IRequirement req, IInstallableUnit iu, List<AbstractVariable> optionalAbstractRequirements, boolean isRootIu) throws ContradictionException {
		if (req.getMax() == 0) {
			expandNegatedRequirement(req, iu, optionalAbstractRequirements, isRootIu);
			return;
		}
		if (!isApplicable(req))
			return;
		List<IInstallableUnit> matches = getApplicableMatches(req);
		determinePotentialHostsForFragment(iu);
		if (req.getMin() > 0) {
			if (matches.isEmpty()) {
				if (iu == entryPoint && emptyBecauseFiltered) {
					dependencyHelper.setFalse(iu, new NotInstallableRoot(req));
				} else {
					missingRequirement(iu, req);
				}
			} else {
				if (req.isGreedy()) {
					IInstallableUnit reqIu = matches.get(0);
					Explanation explanation;
					if (isRootIu) {
						if (alreadyInstalledIUs.contains(reqIu)) {
							explanation = new Explanation.IUInstalled(reqIu);
						} else {
							explanation = new Explanation.IUToInstall(reqIu);
						}
					} else {
						explanation = new Explanation.HardRequirement(iu, req);
					}
					createImplication(iu, matches, explanation);
					IInstallableUnit current;
					for (Iterator<IInstallableUnit> it = matches.iterator(); it.hasNext();) {
						current = it.next();
						if (nonGreedyIUs.contains(current)) {
							addNonGreedyProvider(getNonGreedyVariable(current), iu);
						}
					}
				} else {
					List<Object> newConstraint = new ArrayList<>(matches.size());
					for (IInstallableUnit current : matches) {
						newConstraint.add(getNonGreedyVariable(current));
					}
					createImplication(new Object[] {iu}, newConstraint, new Explanation.HardRequirement(iu, req)); // FIXME
				}
			}
		} else {
			if (!matches.isEmpty()) {
				AbstractVariable abs;
				if (req.isGreedy()) {
					abs = getAbstractVariable(req);
					createImplication(new Object[] {abs, iu}, matches, Explanation.OPTIONAL_REQUIREMENT);
					for (IInstallableUnit current : matches) {
						if (nonGreedyIUs.contains(current)) {
							addNonGreedyProvider(getNonGreedyVariable(current), abs);
						}
					}
					optionalAbstractRequirements.add(abs);
				} else {
					abs = getAbstractVariable(req, false);
					List<Object> newConstraint = new ArrayList<>();
					for (IInstallableUnit current : matches) {
						newConstraint.add(getNonGreedyVariable(current));
					}
					createImplication(new Object[] {abs, iu}, newConstraint, Explanation.OPTIONAL_REQUIREMENT);
				}
			}
		}
	}

	private void addNonGreedyProvider(AbstractVariable nonGreedyVariable, Object o) {
		List<Object> providers = nonGreedyProvider.get(nonGreedyVariable);
		if (providers == null) {
			providers = new ArrayList<>();
			nonGreedyProvider.put(nonGreedyVariable, providers);
		}
		providers.add(o);
	}

	private void expandRequirements(Collection<IRequirement> reqs, IInstallableUnit iu, boolean isRootIu) throws ContradictionException {
		if (reqs.isEmpty())
			return;
		for (IRequirement req : reqs) {
			expandRequirement(req, iu, allOptionalAbstractRequirements, isRootIu);
		}
	}

	public void processIU(IInstallableUnit iu, boolean isRootIU) throws ContradictionException {
		iu = iu.unresolved();
		Map<Version, IInstallableUnit> iuSlice = slice.get(iu.getId());
		if (iuSlice == null) {
			iuSlice = new HashMap<>();
			slice.put(iu.getId(), iuSlice);
		}
		iuSlice.put(iu.getVersion(), iu);
		if (!isApplicable(iu)) {
			createNegation(iu, null);
			return;
		}

		IQueryResult<IInstallableUnit> applicablePatches = getApplicablePatches(iu);
		expandLifeCycle(iu, isRootIU);
		//No patches apply, normal code path
		if (applicablePatches.isEmpty()) {
			expandRequirements(getRequiredCapabilities(iu), iu, isRootIU);
		} else {
			//Patches are applicable to the IU
			expandRequirementsWithPatches(iu, applicablePatches, allOptionalAbstractRequirements, isRootIU);
		}
	}

	private Collection<IRequirement> getRequiredCapabilities(IInstallableUnit iu) {
		boolean isFragment = iu instanceof IInstallableUnitFragment;
		//Short-circuit for the case of an IInstallableUnit
		if ((!isFragment) && iu.getMetaRequirements().size() == 0)
			return iu.getRequirements();

		ArrayList<IRequirement> aggregatedRequirements = new ArrayList<>(iu.getRequirements().size() + iu.getMetaRequirements().size() + (isFragment ? ((IInstallableUnitFragment) iu).getHost().size() : 0));
		aggregatedRequirements.addAll(iu.getRequirements());

		if (iu instanceof IInstallableUnitFragment) {
			aggregatedRequirements.addAll(((IInstallableUnitFragment) iu).getHost());
		}

		if (considerMetaRequirements)
			aggregatedRequirements.addAll(iu.getMetaRequirements());
		return aggregatedRequirements;
	}

	static final class Pending {
		List<? super IInstallableUnitPatch> matches;
		Explanation explanation;
		Object left;
	}

	private void expandRequirementsWithPatches(IInstallableUnit iu, IQueryResult<IInstallableUnit> applicablePatches, List<AbstractVariable> optionalAbstractRequirements, boolean isRootIu) throws ContradictionException {
		//Unmodified dependencies
		Collection<IRequirement> iuRequirements = getRequiredCapabilities(iu);
		Map<IRequirement, List<IInstallableUnitPatch>> unchangedRequirements = new HashMap<>(iuRequirements.size());
		Map<IRequirement, Pending> nonPatchedRequirements = new HashMap<>(iuRequirements.size());
		for (IInstallableUnit iup : applicablePatches) {
			IInstallableUnitPatch patch = (IInstallableUnitPatch) iup;
			IRequirement[][] reqs = mergeRequirements(iu, patch);
			if (reqs.length == 0)
				return;

			// Optional requirements are encoded via:
			// ABS -> (match1(req) or match2(req) or ... or matchN(req))
			// noop(IU)-> ~ABS
			// IU -> (noop(IU) or ABS)
			// Therefore we only need one optional requirement statement per IU
			for (IRequirement[] requirement : reqs) {
				//The requirement is unchanged
				if (requirement[0] == requirement[1]) {
					if (requirement[0].getMax() == 0) {
						expandNegatedRequirement(requirement[0], iu, optionalAbstractRequirements, isRootIu);
						return;
					}
					if (!isApplicable(requirement[0])) {
						continue;
					}
					List<IInstallableUnitPatch> patchesAppliedElseWhere = unchangedRequirements.get(requirement[0]);
					if (patchesAppliedElseWhere == null) {
						patchesAppliedElseWhere = new ArrayList<>();
						unchangedRequirements.put(requirement[0], patchesAppliedElseWhere);
					}
					patchesAppliedElseWhere.add(patch);
					continue;
				}
				//Generate dependency when the patch is applied
				//P1 -> (A -> D) equiv. (P1 & A) -> D
				if (isApplicable(requirement[1])) {
					IRequirement req = requirement[1];
					List<IInstallableUnit> matches = getApplicableMatches(req);
					determinePotentialHostsForFragment(iu);
					if (req.getMin() > 0) {
						if (matches.isEmpty()) {
							missingRequirement(patch, req);
						} else {
							if (req.isGreedy()) {
								IInstallableUnit reqIu = matches.get(0);
								Explanation explanation;
								if (isRootIu) {
									if (alreadyInstalledIUs.contains(reqIu)) {
										explanation = new Explanation.IUInstalled(reqIu);
									} else {
										explanation = new Explanation.IUToInstall(reqIu);
									}
								} else {
									explanation = new Explanation.PatchedHardRequirement(iu, req, patch);
								}
								createImplication(new Object[] {patch, iu}, matches, explanation);
								for (IInstallableUnit current : matches) {
									if (nonGreedyIUs.contains(current)) {
										addNonGreedyProvider(getNonGreedyVariable(current), iu);
									}
								}
							} else {
								List<Object> newConstraint = new ArrayList<>();
								for (IInstallableUnit current : matches) {
									newConstraint.add(getNonGreedyVariable(current));
								}
								createImplication(new Object[] {iu}, newConstraint, new Explanation.HardRequirement(iu, req)); // FIXME
							}
						}
					} else {
						if (!matches.isEmpty()) {
							AbstractVariable abs;
							if (req.isGreedy()) {
								abs = getAbstractVariable(req);
								createImplication(new Object[] {patch, abs, iu}, matches, Explanation.OPTIONAL_REQUIREMENT);
								for (IInstallableUnit current : matches) {
									if (nonGreedyIUs.contains(current)) {
										addNonGreedyProvider(getNonGreedyVariable(current), abs);
									}
								}
								optionalAbstractRequirements.add(abs);
							} else {
								abs = getAbstractVariable(req, false);
								List<Object> newConstraint = new ArrayList<>(matches.size());
								for (IInstallableUnit current : matches) {
									newConstraint.add(getNonGreedyVariable(current));
								}
								createImplication(new Object[] {patch, abs, iu}, newConstraint, Explanation.OPTIONAL_REQUIREMENT);
							}
						}
					}
				}
				//Generate dependency when the patch is not applied
				//-P1 -> (A -> B) ( equiv. A -> (P1 or B) )
				if (isApplicable(requirement[0])) {
					IRequirement req = requirement[0];
					// Fix: if multiple patches apply to the same IU-req, we need to make sure we list each
					// patch as an optional match
					Pending pending = nonPatchedRequirements.get(req);
					if (pending != null) {
						pending.matches.add(patch);
						continue;
					}
					pending = new Pending();
					pending.left = iu;
					List<IInstallableUnit> matches = getApplicableMatches(req);
					determinePotentialHostsForFragment(iu);
					if (req.getMin() > 0) {
						if (matches.isEmpty()) {
							matches.add(patch);
							pending.explanation = new Explanation.HardRequirement(iu, req);
							pending.matches = matches;
						} else {
							// manage non greedy IUs
							List<Object> nonGreedys = new ArrayList<>();
							for (IInstallableUnit current : matches) {
								if (nonGreedyIUs.contains(current)) {
									nonGreedys.add(getNonGreedyVariable(current));
								}
							}
							matches.add(patch);
							if (req.isGreedy()) {
								IInstallableUnit reqIu = matches.get(0);///(IInstallableUnit) picker.query(new CapabilityQuery(req), new Collector(), null).iterator().next();
								Explanation explanation;
								if (isRootIu) {
									if (alreadyInstalledIUs.contains(reqIu)) {
										explanation = new Explanation.IUInstalled(reqIu);
									} else {
										explanation = new Explanation.IUToInstall(reqIu);
									}
								} else {
									explanation = new Explanation.HardRequirement(iu, req);
								}

								// Fix: make sure we collect all patches that will impact this IU-req, not just one
								pending.explanation = explanation;
								pending.matches = matches;
								for (IInstallableUnit current : matches) {
									if (nonGreedyIUs.contains(current)) {
										addNonGreedyProvider(getNonGreedyVariable(current), iu);
									}
								}
							} else {
								List<Object> newConstraint = new ArrayList<>(matches.size());
								for (IInstallableUnit current : matches) {
									newConstraint.add(getNonGreedyVariable(current));
								}
								pending.explanation = new Explanation.HardRequirement(iu, req);
								pending.matches = newConstraint;
							}
							nonPatchedRequirements.put(req, pending);

						}
					} else {
						if (!matches.isEmpty()) {
							AbstractVariable abs;
							matches.add(patch);
							pending = new Pending();
							pending.explanation = Explanation.OPTIONAL_REQUIREMENT;

							if (req.isGreedy()) {
								abs = getAbstractVariable(req);
								// Fix: make sure we collect all patches that will impact this IU-req, not just one
								pending.left = new Object[] {abs, iu};
								pending.matches = matches;
								for (IInstallableUnit current : matches) {
									if (nonGreedyIUs.contains(current)) {
										addNonGreedyProvider(getNonGreedyVariable(current), abs);
									}
								}
							} else {
								abs = getAbstractVariable(req, false);
								List<Object> newConstraint = new ArrayList<>(matches.size());
								for (IInstallableUnit current : matches) {
									newConstraint.add(getNonGreedyVariable(current));
								}
								newConstraint.add(patch);
								pending.left = new Object[] {abs, iu};
								pending.matches = newConstraint;
							}
							nonPatchedRequirements.put(req, pending);
							optionalAbstractRequirements.add(abs);
						}
					}
				}
			}
		}

		// Fix: now create the pending non-patch requirements based on the full set of patches
		for (Pending pending : nonPatchedRequirements.values()) {
			createImplication(pending.left, pending.matches, pending.explanation);
		}

		for (Entry<IRequirement, List<IInstallableUnitPatch>> entry : unchangedRequirements.entrySet()) {
			List<IInstallableUnitPatch> patchesApplied = entry.getValue();
			Iterator<IInstallableUnit> allPatches = applicablePatches.iterator();
			List<IInstallableUnitPatch> requiredPatches = new ArrayList<>();
			while (allPatches.hasNext()) {
				IInstallableUnitPatch patch = (IInstallableUnitPatch) allPatches.next();
				if (!patchesApplied.contains(patch))
					requiredPatches.add(patch);
			}
			IRequirement req = entry.getKey();
			List<IInstallableUnit> matches = getApplicableMatches(req);
			determinePotentialHostsForFragment(iu);
			if (req.getMin() > 0) {
				if (matches.isEmpty()) {
					if (requiredPatches.isEmpty()) {
						missingRequirement(iu, req);
					} else {
						createImplication(iu, requiredPatches, new Explanation.HardRequirement(iu, req));
					}
				} else {
					// manage non greedy IUs
					List<Object> nonGreedys = new ArrayList<>(matches.size());
					for (IInstallableUnit current : matches) {
						if (nonGreedyIUs.contains(current)) {
							nonGreedys.add(getNonGreedyVariable(current));
						}
					}
					if (!requiredPatches.isEmpty())
						matches.addAll(requiredPatches);
					if (req.isGreedy()) {
						IInstallableUnit reqIu = matches.get(0);
						Explanation explanation;
						if (isRootIu) {
							if (alreadyInstalledIUs.contains(reqIu)) {
								explanation = new Explanation.IUInstalled(reqIu);
							} else {
								explanation = new Explanation.IUToInstall(reqIu);
							}
						} else {
							explanation = new Explanation.HardRequirement(iu, req);
						}
						createImplication(iu, matches, explanation);
						for (IInstallableUnit current : matches) {
							if (nonGreedyIUs.contains(current)) {
								addNonGreedyProvider(getNonGreedyVariable(current), iu);
							}
						}
					} else {
						List<Object> newConstraint = new ArrayList<>(matches.size());
						for (IInstallableUnit current : matches) {
							newConstraint.add(getNonGreedyVariable(current));
						}
						createImplication(new Object[] {iu}, newConstraint, new Explanation.HardRequirement(iu, req)); // FIXME
					}
				}
			} else {
				if (!matches.isEmpty()) {
					if (!requiredPatches.isEmpty())
						matches.addAll(requiredPatches);
					AbstractVariable abs;
					if (req.isGreedy()) {
						abs = getAbstractVariable(req);
						createImplication(new Object[] {abs, iu}, matches, Explanation.OPTIONAL_REQUIREMENT);
						for (IInstallableUnit current : matches) {
							if (nonGreedyIUs.contains(current)) {
								addNonGreedyProvider(getNonGreedyVariable(current), iu);
							}
						}
					} else {
						abs = getAbstractVariable(req, false);
						List<Object> newConstraint = new ArrayList<>(matches.size());
						for (IInstallableUnit current : matches) {
							newConstraint.add(getNonGreedyVariable(current));
						}
						createImplication(new Object[] {abs, iu}, newConstraint, new Explanation.HardRequirement(iu, req)); // FIXME
					}
					optionalAbstractRequirements.add(abs);
				}
			}
		}
	}

	private void expandLifeCycle(IInstallableUnit iu, boolean isRootIu) throws ContradictionException {
		if (!(iu instanceof IInstallableUnitPatch))
			return;
		IInstallableUnitPatch patch = (IInstallableUnitPatch) iu;
		IRequirement req = patch.getLifeCycle();
		if (req == null)
			return;
		expandRequirement(req, iu, Collections.emptyList(), isRootIu);
	}

	private void missingRequirement(IInstallableUnit iu, IRequirement req) throws ContradictionException {
		result.add(new Status(IStatus.WARNING, DirectorActivator.PI_DIRECTOR, NLS.bind(Messages.Planner_Unsatisfied_dependency, iu, req)));
		createNegation(iu, req);
	}

	/**
	 * @param req
	 * @return a list of mandatory requirements if any, an empty list if req.isOptional().
	 */
	private List<IInstallableUnit> getApplicableMatches(IRequirement req) {
		List<IInstallableUnit> target = new ArrayList<>();
		IQueryResult<IInstallableUnit> matches = picker.query(QueryUtil.createMatchQuery(req.getMatches()), null);
		for (IInstallableUnit match : matches) {
			if (isApplicable(match)) {
				target.add(match);
			}
		}
		emptyBecauseFiltered = !matches.isEmpty() && target.isEmpty();
		return target;
	}

	//Return a new array of requirements representing the application of the patch
	private IRequirement[][] mergeRequirements(IInstallableUnit iu, IInstallableUnitPatch patch) {
		if (patch == null)
			return null;
		List<IRequirementChange> changes = patch.getRequirementsChange();
		Collection<IRequirement> iuRequirements = iu.getRequirements();
		IRequirement[] originalRequirements = iuRequirements.toArray(new IRequirement[iuRequirements.size()]);
		List<IRequirement[]> rrr = new ArrayList<>();
		boolean found = false;
		for (IRequirementChange change : changes) {
			for (int j = 0; j < originalRequirements.length; j++) {
				if (originalRequirements[j] != null && safeMatch(originalRequirements, change, j)) {
					found = true;
					if (change.newValue() != null)
						rrr.add(new IRequirement[] {originalRequirements[j], change.newValue()});
					else
						// case where a requirement is removed
						rrr.add(new IRequirement[] {originalRequirements[j], null});
					originalRequirements[j] = null;
				}
				//				break;
			}
			if (!found && change.applyOn() == null && change.newValue() != null) //Case where a new requirement is added
				rrr.add(new IRequirement[] {null, change.newValue()});
		}
		//Add all the unmodified requirements to the result
		for (IRequirement originalRequirement : originalRequirements) {
			if (originalRequirement != null)
				rrr.add(new IRequirement[] {originalRequirement, originalRequirement});
		}
		return rrr.toArray(new IRequirement[rrr.size()][]);
	}

	private boolean safeMatch(IRequirement[] originalRequirements, IRequirementChange change, int j) {
		try {
			return change.matches((IRequiredCapability) originalRequirements[j]);
		} catch (ClassCastException e) {
			return false;
		}
	}

	//This will create as many implication as there is element in the right argument
	private void createNegationImplication(Object left, List<?> right, Explanation name) throws ContradictionException {
		if (DEBUG) {
			Tracing.debug(name + ": " + left + "->" + right); //$NON-NLS-1$ //$NON-NLS-2$
		}
		for (Object r : right)
			dependencyHelper.implication(new Object[] {left}).impliesNot(r).named(name);
	}

	private void createImplication(Object left, List<?> right, Explanation name) throws ContradictionException {
		if (DEBUG) {
			Tracing.debug(name + ": " + left + "->" + right); //$NON-NLS-1$ //$NON-NLS-2$
		}
		dependencyHelper.implication(new Object[] {left}).implies(right.toArray()).named(name);
	}

	private void createImplication(Object[] left, List<?> right, Explanation name) throws ContradictionException {
		if (DEBUG) {
			Tracing.debug(name + ": " + Arrays.asList(left) + "->" + right); //$NON-NLS-1$ //$NON-NLS-2$
		}
		dependencyHelper.implication(left).implies(right.toArray()).named(name);
	}

	//Return IUPatches that are applicable for the given iu
	private IQueryResult<IInstallableUnit> getApplicablePatches(IInstallableUnit iu) {
		if (patches == null)
			patches = new QueryableArray(picker.query(QueryUtil.createIUPatchQuery(), null).toArray(IInstallableUnit.class));

		return patches.query(new ApplicablePatchQuery(iu), null);
	}

	//Create constraints to deal with singleton
	//When there is a mix of singleton and non singleton, several constraints are generated
	private void createConstraintsForSingleton() throws ContradictionException {
		Set<Entry<String, Map<Version, IInstallableUnit>>> s = slice.entrySet();
		for (Entry<String, Map<Version, IInstallableUnit>> entry : s) {
			Map<Version, IInstallableUnit> conflictingEntries = entry.getValue();
			if (conflictingEntries.size() < 2)
				continue;

			Collection<IInstallableUnit> conflictingVersions = conflictingEntries.values();
			List<IInstallableUnit> singletons = new ArrayList<>();
			List<IInstallableUnit> nonSingletons = new ArrayList<>();
			for (IInstallableUnit iu : conflictingVersions) {
				if (iu.isSingleton()) {
					singletons.add(iu);
				} else {
					nonSingletons.add(iu);
				}
			}
			if (singletons.isEmpty())
				continue;

			IInstallableUnit[] singletonArray;
			if (nonSingletons.isEmpty()) {
				singletonArray = singletons.toArray(new IInstallableUnit[singletons.size()]);
				createAtMostOne(singletonArray);
			} else {
				singletonArray = singletons.toArray(new IInstallableUnit[singletons.size() + 1]);
				for (IInstallableUnit nonSingleton : nonSingletons) {
					singletonArray[singletonArray.length - 1] = nonSingleton;
					createAtMostOne(singletonArray);
				}
			}
		}
	}

	private void createAtMostOne(IInstallableUnit[] ius) throws ContradictionException {
		if (DEBUG) {
			StringBuilder b = new StringBuilder();
			for (IInstallableUnit iu : ius) {
				b.append(iu.toString());
			}
			Tracing.debug("At most 1 of " + b); //$NON-NLS-1$
		}
		dependencyHelper.atMost(1, (Object[]) ius).named(new Explanation.Singleton(ius));
	}

	private AbstractVariable getAbstractVariable(IRequirement req) {
		return getAbstractVariable(req, true);
	}

	private AbstractVariable getAbstractVariable(IRequirement req, boolean appearInOptFunction) {
		AbstractVariable abstractVariable = DEBUG_ENCODING ? new AbstractVariable("Abs_" + req.toString()) : new AbstractVariable(); //$NON-NLS-1$
		if (appearInOptFunction) {
			abstractVariables.add(abstractVariable);
		}
		return abstractVariable;
	}

	private AbstractVariable getNonGreedyVariable(IInstallableUnit iu) {
		AbstractVariable v = nonGreedyVariables.get(iu);
		if (v == null) {
			v = DEBUG_ENCODING ? new AbstractVariable("NG_" + iu.toString()) : new AbstractVariable(); //$NON-NLS-1$
			nonGreedyVariables.put(iu, v);
		}
		return v;
	}

	public IStatus invokeSolver(IProgressMonitor monitor) {
		if (result.getSeverity() == IStatus.ERROR)
			return result;
		// CNF filename is given on the command line
		long start = System.currentTimeMillis();
		if (DEBUG)
			Tracing.debug("Invoking solver: " + start); //$NON-NLS-1$
		try {
			if (monitor.isCanceled())
				return Status.CANCEL_STATUS;
			if (dependencyHelper.hasASolution(assumptions)) {
				if (DEBUG) {
					Tracing.debug("Satisfiable !"); //$NON-NLS-1$
				}
				backToIU();
				long stop = System.currentTimeMillis();
				if (DEBUG)
					Tracing.debug("Solver solution found in: " + (stop - start) + " ms."); //$NON-NLS-1$ //$NON-NLS-2$
			} else {
				long stop = System.currentTimeMillis();
				if (DEBUG) {
					Tracing.debug("Unsatisfiable !"); //$NON-NLS-1$
					Tracing.debug("Solver solution NOT found: " + (stop - start)); //$NON-NLS-1$
				}
				result = new MultiStatus(DirectorActivator.PI_DIRECTOR, SimplePlanner.UNSATISFIABLE, result.getChildren(), Messages.Planner_Unsatisfiable_problem, null);
				result.merge(new Status(IStatus.ERROR, DirectorActivator.PI_DIRECTOR, SimplePlanner.UNSATISFIABLE, Messages.Planner_Unsatisfiable_problem, null));
			}
		} catch (TimeoutException e) {
			result.merge(new Status(IStatus.ERROR, DirectorActivator.PI_DIRECTOR, Messages.Planner_Timeout));
		} catch (Exception e) {
			result.merge(new Status(IStatus.ERROR, DirectorActivator.PI_DIRECTOR, Messages.Planner_Unexpected_problem, e));
		}
		if (DEBUG)
			System.out.println();
		return result;
	}

	private void backToIU() {
		solution = new ArrayList<>();
		IVec<Object> sat4jSolution = dependencyHelper.getSolution();
		for (Iterator<Object> iter = sat4jSolution.iterator(); iter.hasNext();) {
			Object var = iter.next();
			if (var instanceof IInstallableUnit) {
				IInstallableUnit iu = (IInstallableUnit) var;
				if (iu == entryPoint)
					continue;
				solution.add(iu);
			}
		}
	}

	private void printSolution(Collection<IInstallableUnit> state) {
		ArrayList<IInstallableUnit> l = new ArrayList<>(state);
		l.sort(null);
		Tracing.debug("Solution:"); //$NON-NLS-1$
		Tracing.debug("Numbers of IUs selected: " + l.size()); //$NON-NLS-1$
		for (IInstallableUnit s : l) {
			Tracing.debug(s.toString());
		}
	}

	public Collection<IInstallableUnit> extractSolution() {
		if (DEBUG)
			printSolution(solution);
		return solution;
	}

	public Set<Explanation> getExplanation(IProgressMonitor monitor) {
		ExplanationJob job = new ExplanationJob();
		job.schedule();
		monitor.setTaskName(Messages.Planner_NoSolution);
		IProgressMonitor pm = new InfiniteProgress(monitor);
		pm.beginTask(Messages.Planner_NoSolution, 1000);
		try {
			synchronized (job) {
				while (job.getExplanationResult() == null && job.getState() != Job.NONE) {
					if (monitor.isCanceled()) {
						job.cancel();
						throw new OperationCanceledException();
					}
					pm.worked(1);
					try {
						job.wait(100);
					} catch (InterruptedException e) {
						if (DEBUG)
							Tracing.debug("Interrupted while computing explanations"); //$NON-NLS-1$
					}
				}
			}
		} finally {
			monitor.done();
		}
		return job.getExplanationResult();
	}

	public Map<IInstallableUnitFragment, List<IInstallableUnit>> getFragmentAssociation() {
		Map<IInstallableUnitFragment, List<IInstallableUnit>> resolvedFragments = new HashMap<>(fragments.size());
		for (Entry<IInstallableUnitFragment, Set<IInstallableUnit>> fragment : fragments.entrySet()) {
			if (!dependencyHelper.getBooleanValueFor(fragment.getKey()))
				continue;
			Set<IInstallableUnit> potentialHosts = fragment.getValue();
			List<IInstallableUnit> resolvedHost = new ArrayList<>(potentialHosts.size());
			for (IInstallableUnit host : potentialHosts) {
				if (dependencyHelper.getBooleanValueFor(host))
					resolvedHost.add(host);
			}
			if (resolvedHost.size() != 0)
				resolvedFragments.put(fragment.getKey(), resolvedHost);
		}
		return resolvedFragments;
	}

	private void rememberHostMatches(IInstallableUnitFragment fragment, List<IInstallableUnit> matches) {
		Set<IInstallableUnit> existingMatches = fragments.get(fragment);
		if (existingMatches == null) {
			existingMatches = new HashSet<>();
			fragments.put(fragment, existingMatches);
			existingMatches.addAll(matches);
		}
		existingMatches.retainAll(matches);
	}

	public void setUserDefined(boolean containsKey) {
		userDefinedFunction = containsKey;
	}

	public void close() {
		if (dependencyHelper != null) {
			dependencyHelper.reset();
			dependencyHelper = null;
		}
	}
}