/*******************************************************************************
 *  Copyright (c) 2007, 2020 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
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.director;

import java.util.*;
import org.eclipse.equinox.p2.engine.IProvisioningPlan;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.query.*;

public class OperationGenerator {
	private static final IInstallableUnit NULL_IU = MetadataFactory.createResolvedInstallableUnit(MetadataFactory.createInstallableUnit(new MetadataFactory.InstallableUnitDescription()), new IInstallableUnitFragment[0]);
	private final IProvisioningPlan plan;

	public OperationGenerator(IProvisioningPlan plan) {
		this.plan = plan;
	}

	public void generateOperation(Collection<IInstallableUnit> from_, Collection<IInstallableUnit> to_) {
		Collection<IInstallableUnit> intersection = new HashSet<>(from_);
		intersection.retainAll(to_);

		HashSet<IInstallableUnit> tmpFrom = new HashSet<>(from_);
		HashSet<IInstallableUnit> tmpTo = new HashSet<>(to_);
		tmpFrom.removeAll(intersection);
		tmpTo.removeAll(intersection);

		List<IInstallableUnit> from = new ArrayList<>(tmpFrom);
		from.sort(null);

		List<IInstallableUnit> to = new ArrayList<>(tmpTo);
		to.sort(null);

		generateUpdates(from, to);
		generateInstallUninstall(from, to);
		generateConfigurationChanges(to_, intersection);
	}

	//This generates operations that are causing the IUs to be reconfigured.
	private void generateConfigurationChanges(Collection<IInstallableUnit> to_, Collection<IInstallableUnit> intersection) {
		if (intersection.size() == 0)
			return;
		//We retain from each set the things that are the same.
		//Note that despite the fact that they are the same, a different CU can be attached.
		//The objects contained in the intersection are the one that were originally in the from collection.
		TreeSet<IInstallableUnit> to = new TreeSet<>(to_);
		for (IInstallableUnit fromIU : intersection) {
			IInstallableUnit toIU = to.tailSet(fromIU).first();
			generateConfigurationOperation(fromIU, toIU);
		}

	}

	private void generateConfigurationOperation(IInstallableUnit fromIU, IInstallableUnit toIU) {
		Collection<IInstallableUnitFragment> fromFragments = fromIU.getFragments();
		Collection<IInstallableUnitFragment> toFragments = toIU.getFragments();
		if (fromFragments == toFragments)
			return;
		//Check to see if the two arrays are equals independently of the order of the fragments
		if (fromFragments.size() == toFragments.size() && fromFragments.containsAll(toFragments))
			return;
		plan.updateInstallableUnit(fromIU, toIU);
	}

	private void generateInstallUninstall(List<IInstallableUnit> from, List<IInstallableUnit> to) {
		int toIdx = 0;
		int fromIdx = 0;
		while (fromIdx != from.size() && toIdx != to.size()) {
			IInstallableUnit fromIU = from.get(fromIdx);
			IInstallableUnit toIU = to.get(toIdx);
			int comparison = toIU.compareTo(fromIU);
			if (comparison < 0) {
				plan.addInstallableUnit(toIU);
				toIdx++;
			} else if (comparison == 0) {
				toIdx++;
				fromIdx++;
				//				System.out.println("same " + fromIU);
			} else {
				plan.removeInstallableUnit(fromIU);
				fromIdx++;
			}
		}
		if (fromIdx != from.size()) {
			for (int i = fromIdx; i < from.size(); i++) {
				plan.removeInstallableUnit(from.get(i));
			}
		}
		if (toIdx != to.size()) {
			for (int i = toIdx; i < to.size(); i++) {
				plan.addInstallableUnit(to.get(i));
			}
		}
	}

	private void generateUpdates(List<IInstallableUnit> from, List<IInstallableUnit> to) {
		if (to.isEmpty() || from.isEmpty())
			return;

		Set<IInstallableUnit> processed = new HashSet<>();
		Set<IInstallableUnit> removedFromTo = new HashSet<>();

		QueryableArray indexedFromElements = new QueryableArray(from.toArray(new IInstallableUnit[from.size()]));
		for (int toIdx = 0; toIdx < to.size(); toIdx++) {
			IInstallableUnit iuTo = to.get(toIdx);
			if (iuTo.getId().equals(next(to, toIdx).getId())) { //This handle the case where there are multiple versions of the same IU in the target. Eg we are trying to update from A 1.0.0 to A 1.1.1 and A 1.2.2
				toIdx = skip(to, iuTo, toIdx) - 1;
				//System.out.println("Can't update " + iuTo + " because another iu with same id is in the target state");
				continue;
			}
			if (iuTo.getUpdateDescriptor() == null)
				continue;

			//TODO we eventually need to handle the case where an IU is a merge of several others.

			IQuery<IInstallableUnit> updateQuery = QueryUtil.createMatchQuery(iuTo.getUpdateDescriptor().getIUsBeingUpdated().iterator().next(), new Object[0]);
			iuTo.getUpdateDescriptor().getIUsBeingUpdated();
			IQueryResult<IInstallableUnit> updates = indexedFromElements.query(updateQuery, null);

			if (updates.isEmpty()) { //Nothing to update from.
				continue;
			}
			Iterator<IInstallableUnit> updatesIterator = updates.iterator();
			IInstallableUnit iuFrom = updatesIterator.next();
			if (updatesIterator.hasNext()) { //There are multiple IUs to update from
				//System.out.println("Can't update  " + iuTo + " because there are multiple IUs to update from (" + toString(iusFrom) + ')');
				continue;
			}
			if (iuTo.equals(iuFrom)) {
				from.remove(iuFrom);
				//				fromIdIndexList.remove(iuFrom);
				removedFromTo.add(iuTo);
				continue;
			}
			plan.updateInstallableUnit(iuFrom, iuTo);
			from.remove(iuFrom);
			//			fromIdIndexList.remove(iuFrom);
			processed.add(iuTo);
		}
		to.removeAll(processed);
		to.removeAll(removedFromTo);
	}

	private IInstallableUnit next(List<IInstallableUnit> l, int i) {
		i++;
		if (i >= l.size())
			return NULL_IU;
		return l.get(i);
	}

	private int skip(List<IInstallableUnit> c, IInstallableUnit id, int idx) {
		int i = idx;
		for (; i < c.size(); i++) {
			if (!id.getId().equals(c.get(i).getId()))
				return i;
		}
		return i;
	}
}
