/*******************************************************************************
 * Copyright (c) 2009, 2020 Daniel Le Berre 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:
 *     Daniel Le Berre - initial API and implementation
 *     Red Hat, Inc. - support for remediation page
 ******************************************************************************/
package org.eclipse.equinox.internal.p2.director;

import java.math.BigInteger;
import java.util.*;
import org.eclipse.equinox.internal.p2.director.Projector.AbstractVariable;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.query.*;
import org.sat4j.pb.tools.*;
import org.sat4j.specs.ContradictionException;

public class UserDefinedOptimizationFunction extends OptimizationFunction {
	private Collection<IInstallableUnit> alreadyExistingRoots;
	private SteppedTimeoutLexicoHelper<Object, Explanation> dependencyHelper;
	private IQueryable<IInstallableUnit> picker;

	public UserDefinedOptimizationFunction(IQueryable<IInstallableUnit> lastState, List<AbstractVariable> abstractVariables, List<AbstractVariable> optionalVariables, IQueryable<IInstallableUnit> picker, IInstallableUnit selectionContext, Map<String, Map<Version, IInstallableUnit>> slice, DependencyHelper<Object, Explanation> dependencyHelper, Collection<IInstallableUnit> alreadyInstalledIUs) {
		super(lastState, abstractVariables, optionalVariables, picker, selectionContext, slice);
		this.picker = picker;
		this.slice = slice;
		this.dependencyHelper = (SteppedTimeoutLexicoHelper<Object, Explanation>) dependencyHelper;
		this.alreadyExistingRoots = alreadyInstalledIUs;
	}

	@Override
	public List<WeightedObject<? extends Object>> createOptimizationFunction(IInstallableUnit metaIu, Collection<IInstallableUnit> newRoots) {
		List<WeightedObject<?>> weightedObjects = new ArrayList<>();
		List<Object> objects = new ArrayList<>();
		BigInteger weight = BigInteger.valueOf(slice.size() + 1);
		String[] criterias = new String[] {"+new", "-notuptodate", "-changed", "-removed"}; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
		BigInteger currentWeight = weight.pow(criterias.length - 1);
		boolean maximizes;
		Object thing;
		for (String criteria : criterias) {
			if (criteria.endsWith("new")) { //$NON-NLS-1$
				weightedObjects.clear();
				newRoots(weightedObjects, criteria.startsWith("+") ? currentWeight.negate() : currentWeight, metaIu); //$NON-NLS-1$
				currentWeight = currentWeight.divide(weight);
			} else if (criteria.endsWith("removed")) { //$NON-NLS-1$
				weightedObjects.clear();
				removedRoots(weightedObjects, criteria.startsWith("+") ? currentWeight.negate() : currentWeight, metaIu); //$NON-NLS-1$
				currentWeight = currentWeight.divide(weight);
			} else if (criteria.endsWith("notuptodate")) { //$NON-NLS-1$
				weightedObjects.clear();
				notuptodate(weightedObjects, criteria.startsWith("+") ? currentWeight.negate() : currentWeight, metaIu); //$NON-NLS-1$
				currentWeight = currentWeight.divide(weight);
			} else if (criteria.endsWith("changed")) { //$NON-NLS-1$
				weightedObjects.clear();
				changedRoots(weightedObjects, criteria.startsWith("+") ? currentWeight.negate() : currentWeight, metaIu); //$NON-NLS-1$
				currentWeight = currentWeight.divide(weight);
			}
			objects.clear();
			maximizes = criteria.startsWith("+"); //$NON-NLS-1$
			for (WeightedObject<?> weightedObject : weightedObjects) {
				thing = weightedObject.thing;
				if (maximizes) {
					thing = dependencyHelper.not(thing);
				}
				objects.add(thing);
			}
			dependencyHelper.addCriterion(objects);
		}
		weightedObjects.clear();
		return null;
	}

	protected void changedRoots(List<WeightedObject<?>> weightedObjects, BigInteger weight, IInstallableUnit entryPointIU) {
		Collection<IRequirement> requirements = entryPointIU.getRequirements();
		for (IRequirement req : requirements) {
			IQuery<IInstallableUnit> query = QueryUtil.createMatchQuery(req.getMatches());
			IQueryResult<IInstallableUnit> matches = picker.query(query, null);
			Object[] changed = new Object[matches.toUnmodifiableSet().size()];
			int i = 0;
			for (IInstallableUnit match : matches) {
				changed[i++] = isInstalledAsRoot(match) ? dependencyHelper.not(match) : match;
			}
			try {
				Projector.AbstractVariable abs = new Projector.AbstractVariable("CHANGED"); //$NON-NLS-1$
				dependencyHelper.or(FakeExplanation.getInstance(), abs, changed);
				weightedObjects.add(WeightedObject.newWO(abs, weight));
			} catch (ContradictionException e) {
				// TODO Auto-generated catch block TODO
				e.printStackTrace();
			}
		}
	}

	protected void newRoots(List<WeightedObject<?>> weightedObjects, BigInteger weight, IInstallableUnit entryPointIU) {
		Collection<IRequirement> requirements = entryPointIU.getRequirements();
		for (IRequirement req : requirements) {
			IQuery<IInstallableUnit> query = QueryUtil.createMatchQuery(req.getMatches());
			IQueryResult<IInstallableUnit> matches = picker.query(query, null);
			boolean oneInstalled = false;
			for (IInstallableUnit match : matches) {
				oneInstalled = oneInstalled || isInstalledAsRoot(match);
			}
			if (!oneInstalled) {
				try {
					Projector.AbstractVariable abs = new Projector.AbstractVariable("NEW"); //$NON-NLS-1$
					dependencyHelper.or(FakeExplanation.getInstance(), abs, (Object[]) matches.toArray(IInstallableUnit.class));
					weightedObjects.add(WeightedObject.newWO(abs, weight));
				} catch (ContradictionException e) {
					// should not happen
					e.printStackTrace();
				}
			}
		}
	}

	protected void removedRoots(List<WeightedObject<?>> weightedObjects, BigInteger weight, IInstallableUnit entryPointIU) {
		Collection<IRequirement> requirements = entryPointIU.getRequirements();
		for (IRequirement req : requirements) {
			IQuery<IInstallableUnit> query = QueryUtil.createMatchQuery(req.getMatches());
			IQueryResult<IInstallableUnit> matches = picker.query(query, null);
			boolean installed = false;
			Object[] literals = new Object[matches.toUnmodifiableSet().size()];
			int i = 0;
			for (IInstallableUnit match : matches) {
				installed = installed || isInstalledAsRoot(match);
				literals[i++] = dependencyHelper.not(match);
			}
			if (installed) {
				try {
					Projector.AbstractVariable abs = new Projector.AbstractVariable("REMOVED"); //$NON-NLS-1$
					dependencyHelper.and(FakeExplanation.getInstance(), abs, literals);
					weightedObjects.add(WeightedObject.newWO(abs, weight));
				} catch (ContradictionException e) {
					// should not happen TODO
					e.printStackTrace();
				}
			}
		}
	}

	protected void notuptodate(List<WeightedObject<?>> weightedObjects, BigInteger weight, IInstallableUnit entryPointIU) {
		Collection<IRequirement> requirements = entryPointIU.getRequirements();
		for (IRequirement req : requirements) {
			IQuery<IInstallableUnit> query = QueryUtil.createMatchQuery(req.getMatches());
			IQueryResult<IInstallableUnit> matches = picker.query(query, null);
			List<IInstallableUnit> toSort = new ArrayList<>(matches.toUnmodifiableSet());
			toSort.sort(Collections.reverseOrder());
			if (toSort.isEmpty())
				continue;

			Projector.AbstractVariable abs = new Projector.AbstractVariable();
			Object notlatest = dependencyHelper.not(toSort.get(0));
			try {
				// notuptodate <=> not iuvn and (iuv1 or iuv2 or ... iuvn-1)
				dependencyHelper.implication(new Object[] {abs}).implies(notlatest).named(FakeExplanation.getInstance());
				Object[] clause = new Object[toSort.size()];
				toSort.toArray(clause);
				clause[0] = dependencyHelper.not(abs);
				dependencyHelper.clause(FakeExplanation.getInstance(), clause);
				for (int i = 1; i < toSort.size(); i++) {
					dependencyHelper.implication(new Object[] {notlatest, toSort.get(i)}).implies(abs).named(FakeExplanation.getInstance());
				}
			} catch (ContradictionException e) {
				// should never happen
				e.printStackTrace();
			}

			weightedObjects.add(WeightedObject.newWO(abs, weight));
		}
	}

	private static class FakeExplanation extends Explanation {
		private static Explanation singleton = new FakeExplanation();

		public static Explanation getInstance() {
			return singleton;
		}

		@Override
		protected int orderValue() {
			return Explanation.OTHER_REASON;
		}

		@Override
		public int shortAnswer() {
			return 0;
		}

	}

	private boolean isInstalledAsRoot(IInstallableUnit isInstalled) {
		for (IInstallableUnit installed : alreadyExistingRoots) {
			if (isInstalled.equals(installed))
				return true;
		}
		return false;
	}

}
