/*******************************************************************************
 * Copyright (c) 2000, 2011 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.jdt.internal.corext.refactoring.generics;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.ArrayType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.HierarchyType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.typesets.EnumeratedTypeSet;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.typesets.SingletonTypeSet;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.typesets.TypeSet;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.typesets.TypeSetEnvironment;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ArrayElementVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ArrayTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.CastVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.CollectionElementVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ConstraintVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ITypeConstraint2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.IndependentTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.TTypes;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.TypeEquivalenceSet;


public class InferTypeArgumentsConstraintsSolver {

	private static class TTypeComparator implements Comparator<TType> {
		@Override
		public int compare(TType o1, TType o2) {
			return o1.getPrettySignature().compareTo(o2.getPrettySignature());
		}
		public static TTypeComparator INSTANCE= new TTypeComparator();
	}

	private final static String CHOSEN_TYPE= "chosenType"; //$NON-NLS-1$

	private final InferTypeArgumentsTCModel fTCModel;
	private TypeSetEnvironment fTypeSetEnvironment;

	/**
	 * The work-list used by the type constraint solver to hold the set of
	 * nodes in the constraint graph that remain to be (re-)processed. Entries
	 * are <code>ConstraintVariable2</code>s.
	 */
	private LinkedList<ConstraintVariable2> fWorkList;

	private InferTypeArgumentsUpdate fUpdate;


	public InferTypeArgumentsConstraintsSolver(InferTypeArgumentsTCModel typeConstraintFactory) {
		fTCModel= typeConstraintFactory;
		fWorkList= new LinkedList<>();
	}

	public InferTypeArgumentsUpdate solveConstraints(IProgressMonitor pm) {
		pm.beginTask("", 2); //$NON-NLS-1$
		fUpdate= new InferTypeArgumentsUpdate();

		ConstraintVariable2[] allConstraintVariables= fTCModel.getAllConstraintVariables();
		if (allConstraintVariables.length == 0)
			return fUpdate;

		fTypeSetEnvironment= new TypeSetEnvironment(fTCModel.getTypeEnvironment());
		ParametricStructureComputer parametricStructureComputer= new ParametricStructureComputer(allConstraintVariables, fTCModel);
		Collection<CollectionElementVariable2> newVars= parametricStructureComputer.createElemConstraintVariables();

		ArrayList<ConstraintVariable2> newAllConstraintVariables= new ArrayList<>(Arrays.asList(allConstraintVariables));
		newAllConstraintVariables.addAll(newVars);
		allConstraintVariables= newAllConstraintVariables.toArray(new ConstraintVariable2[newAllConstraintVariables.size()]);


		//loop over all TypeEquivalenceSets and unify the elements from the fElemStructureEnv with the existing TypeEquivalenceSets
		HashSet<TypeEquivalenceSet> allTypeEquivalenceSets= new HashSet<>();
		for (ConstraintVariable2 allConstraintVariable : allConstraintVariables) {
			TypeEquivalenceSet typeEquivalenceSet= allConstraintVariable.getTypeEquivalenceSet();
			if (typeEquivalenceSet != null)
				allTypeEquivalenceSets.add(typeEquivalenceSet);
		}
		for (TypeEquivalenceSet typeEquivalenceSet : allTypeEquivalenceSets) {
			ConstraintVariable2[] contributingVariables= typeEquivalenceSet.getContributingVariables();
			for (int i= 0; i < contributingVariables.length; i++) {
				for (int j= i + 1; j < contributingVariables.length; j++) {
					ConstraintVariable2 first= contributingVariables[i];
					ConstraintVariable2 second= contributingVariables[j];
					fTCModel.createElementEqualsConstraints(first, second); // recursively
				}
			}
		}
		for (ITypeConstraint2 typeConstraint : fTCModel.getAllTypeConstraints()) {
			fTCModel.createElementEqualsConstraints(typeConstraint.getLeft(), typeConstraint.getRight());
		}

		initializeTypeEstimates(allConstraintVariables);
		if (pm.isCanceled())
			throw new OperationCanceledException();
		fWorkList.addAll(Arrays.asList(allConstraintVariables));
		runSolver(new SubProgressMonitor(pm, 1));
		chooseTypes(allConstraintVariables, new SubProgressMonitor(pm, 1));
		findCastsToRemove(fTCModel.getCastVariables());
		return fUpdate;
	}

	private void initializeTypeEstimates(ConstraintVariable2[] allConstraintVariables) {
		for (ConstraintVariable2 cv : allConstraintVariables) {
			//TODO: not necessary for types that are not used in a TypeConstraint but only as type in CollectionElementVariable
			//TODO: handle nested element variables; see ParametricStructureComputer.createAndInitVars()
			TypeEquivalenceSet set= cv.getTypeEquivalenceSet();
			if (set == null) {
				set= new TypeEquivalenceSet(cv);
				set.setTypeEstimate(createInitialEstimate(cv));
				cv.setTypeEquivalenceSet(set);
			} else {
				TypeSet typeEstimate= (TypeSet) cv.getTypeEstimate();
				if (typeEstimate == null) {
					typeEstimate= fTypeSetEnvironment.getUniverseTypeSet();
					for (ConstraintVariable2 c : set.getContributingVariables()) { //TODO: optimize: just try to find an immutable CV; if not found, use Universe
						typeEstimate= typeEstimate.intersectedWith(createInitialEstimate(c));
					}
					set.setTypeEstimate(typeEstimate);
				}
			}
		}
	}

	private TypeSet createInitialEstimate(ConstraintVariable2 cv) {
		// TODO: check assumption: only immutable CVs have a type
//		ParametricStructure parametricStructure= fElemStructureEnv.elemStructure(cv);
//		if (parametricStructure != null && parametricStructure != ParametricStructureComputer.ParametricStructure.NONE) {
//			return SubTypesOfSingleton.create(parametricStructure.getBase());
//		}

		TType type= cv.getType();
		if (type == null
				|| cv instanceof IndependentTypeVariable2
				|| cv instanceof ArrayTypeVariable2) {
			return fTypeSetEnvironment.getUniverseTypeSet();
			//TODO: solve problem with recursive bounds
//			TypeVariable tv= (TypeVariable) type;
//			TType[] bounds= tv.getBounds();
//			TypeSet result= SubTypesOfSingleton.create(bounds[0].getErasure());
//			for (int i= 1; i < bounds.length; i++) {
//				result= result.intersectedWith(SubTypesOfSingleton.create(bounds[i].getErasure()));
//			}
//			return result;

		} else if (cv instanceof ArrayElementVariable2) {
			if (cv.getType() != null && cv.getType().isTypeVariable()) {
				return fTypeSetEnvironment.getUniverseTypeSet();
			} else {
				return new SingletonTypeSet(type, fTypeSetEnvironment);
			}

		} else if (type.isVoidType()) {
			return fTypeSetEnvironment.getEmptyTypeSet();
		} else {
			return new SingletonTypeSet(type, fTypeSetEnvironment);
		}
	}

	private void runSolver(SubProgressMonitor pm) {
		pm.beginTask("", fWorkList.size() * 3); //$NON-NLS-1$
		while (! fWorkList.isEmpty()) {
			// Get a variable whose type estimate has changed
			ConstraintVariable2 cv= fWorkList.removeFirst();
			List<ITypeConstraint2> usedIn= fTCModel.getUsedIn(cv);
			processConstraints(usedIn);
			pm.worked(1);
			if (pm.isCanceled())
				throw new OperationCanceledException();
		}
		pm.done();
	}

	/**
	 * Given a list of <code>ITypeConstraint2</code>s that all refer to a
	 * given <code>ConstraintVariable2</code> (whose type bound has presumably
	 * just changed), process each <code>ITypeConstraint</code>, propagating
	 * the type bound across the constraint as needed.
	 *
	 * @param usedIn the <code>List</code> of <code>ITypeConstraint2</code>s
	 * to process
	 */
	private void processConstraints(List<ITypeConstraint2> usedIn) {
		Iterator<ITypeConstraint2> iter= usedIn.iterator();
		while (iter.hasNext()) {
			ITypeConstraint2 tc= iter.next();

				maintainSimpleConstraint(tc);
				//TODO: prune tcs which cannot cause further changes
				// Maybe these should be pruned after a special first loop over all ConstraintVariables,
				// Since this can only happen once for every CV in the work list.
//				if (isConstantConstraint(stc))
//					fTypeConstraintFactory.removeUsedIn(stc, changedCv);
		}
	}

	private void maintainSimpleConstraint(ITypeConstraint2 stc) {
		ConstraintVariable2 left= stc.getLeft();
		ConstraintVariable2 right= stc.getRight();

		TypeEquivalenceSet leftSet= left.getTypeEquivalenceSet();
		TypeEquivalenceSet rightSet= right.getTypeEquivalenceSet();
		TypeSet leftEstimate= (TypeSet) leftSet.getTypeEstimate();
		TypeSet rightEstimate= (TypeSet) rightSet.getTypeEstimate();

		if (leftEstimate.isUniverse() && rightEstimate.isUniverse())
			return; // nothing to do

		if (leftEstimate.equals(rightEstimate))
			return; // nothing to do

		TypeSet lhsSuperTypes= leftEstimate.superTypes();
		TypeSet rhsSubTypes= rightEstimate.subTypes();

		if (! rhsSubTypes.containsAll(leftEstimate)) {
			TypeSet xsection= leftEstimate.intersectedWith(rhsSubTypes);

//			if (xsection.isEmpty()) // too bad, but this can happen
//				throw new IllegalStateException("Type estimate set is now empty for LHS in " + left + " <= " + right + "; estimates were " + leftEstimate + " <= " + rightEstimate); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$

			leftSet.setTypeEstimate(xsection);
			fWorkList.addAll(Arrays.asList(leftSet.getContributingVariables()));
		}
		if (! lhsSuperTypes.containsAll(rightEstimate)) {
			TypeSet xsection= rightEstimate.intersectedWith(lhsSuperTypes);

//			if (xsection.isEmpty())
//				throw new IllegalStateException("Type estimate set is now empty for RHS in " + left + " <= " + right + "; estimates were " + leftEstimate + " <= " + rightEstimate); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$

			rightSet.setTypeEstimate(xsection);
			fWorkList.addAll(Arrays.asList(rightSet.getContributingVariables()));
		}
	}

	private void chooseTypes(ConstraintVariable2[] allConstraintVariables, SubProgressMonitor pm) {
		pm.beginTask("", allConstraintVariables.length); //$NON-NLS-1$
		for (ConstraintVariable2 cv : allConstraintVariables) {
			TypeEquivalenceSet set= cv.getTypeEquivalenceSet();
			if (set == null)
				continue; //TODO: should not happen iff all unused constraint variables got pruned
			//TODO: should calculate only once per EquivalenceRepresentative; can throw away estimate TypeSet afterwards
			TType type= chooseSingleType((TypeSet) cv.getTypeEstimate()); //TODO: is null for Universe TypeSet
			setChosenType(cv, type);

			if (cv instanceof CollectionElementVariable2) {
				CollectionElementVariable2 elementCv= (CollectionElementVariable2) cv;
				fUpdate.addDeclaration(elementCv);
			}

			pm.worked(1);
			if (pm.isCanceled())
				throw new OperationCanceledException();
		}
		pm.done();
	}

	private TType chooseSingleType(TypeSet typeEstimate) {
		if (typeEstimate.isUniverse() || typeEstimate.isEmpty()) {
			return null;

		} else if (typeEstimate.hasUniqueLowerBound()) {
			return typeEstimate.uniqueLowerBound();

		} else {
			EnumeratedTypeSet lowerBound= typeEstimate.lowerBound().enumerate();
			ArrayList<TType> interfaceCandidates= null;
			for (Iterator<TType> iter= lowerBound.iterator(); iter.hasNext();) {
				TType type= iter.next();
				if (! type.isInterface()) {
					return type;
				} else {
					if (interfaceCandidates == null)
						interfaceCandidates= new ArrayList<>(2);
					interfaceCandidates.add(type);
				}
			}

			if (interfaceCandidates == null || interfaceCandidates.isEmpty()) {
				return null;
			} else if (interfaceCandidates.size() == 1) {
				return interfaceCandidates.get(0);
			} else {
				ArrayList<TType> nontaggingCandidates= getNonTaggingInterfaces(interfaceCandidates);
				if (!nontaggingCandidates.isEmpty()) {
					return Collections.min(nontaggingCandidates, TTypeComparator.INSTANCE);
				} else {
					return Collections.min(interfaceCandidates, TTypeComparator.INSTANCE);
				}
			}
		}
	}

	private static final int MAX_CACHE= 1024;
	private Map<TType, Boolean> fInterfaceTaggingCache= new LinkedHashMap<>(MAX_CACHE, 0.75f, true) {
		private static final long serialVersionUID= 1L;
		@Override
		protected boolean removeEldestEntry(Map.Entry<TType, Boolean> eldest) {
			return size() > MAX_CACHE;
		}
	};

	private ArrayList<TType> getNonTaggingInterfaces(ArrayList<TType> interfaceCandidates) {
		ArrayList<TType> unresolvedTypes= new ArrayList<>();
		ArrayList<TType> nonTagging= new ArrayList<>();

		for (int i= 0; i < interfaceCandidates.size(); i++) {
			TType interf= interfaceCandidates.get(i);
			Boolean isTagging= fInterfaceTaggingCache.get(interf);
			if (isTagging == null)
				unresolvedTypes.add(interf);
			else if (Boolean.FALSE.equals(isTagging))
				nonTagging.add(interf);
		}

		if (!unresolvedTypes.isEmpty()) {
			for (TType interf : unresolvedTypes) {
				if (isTaggingInterface(interf)) {
					fInterfaceTaggingCache.put(interf, Boolean.TRUE);
				} else {
					fInterfaceTaggingCache.put(interf, Boolean.FALSE);
					nonTagging.add(interf);
				}
			}
		}

		return nonTagging;
	}

	private static boolean isTaggingInterface(TType interf) {
		if (interf instanceof HierarchyType) {
			try {
				return ((HierarchyType) interf).getJavaElementType().getMethods().length == 0;
			} catch (JavaModelException e) {
				// assume it's not
			}
		}
		return false;
	}

	private void findCastsToRemove(CastVariable2[] castVariables) {
		for (CastVariable2 castCv : castVariables) {
			ConstraintVariable2 expressionVariable= castCv.getExpressionVariable();
			TType chosenType= InferTypeArgumentsConstraintsSolver.getChosenType(expressionVariable);
			TType castType= castCv.getType();
			TType expressionType= expressionVariable.getType();
			if (chosenType != null && TTypes.canAssignTo(chosenType, castType)) {
				if (chosenType.equals(expressionType))
					continue; // The type has not changed. Don't remove the cast, since it could be
							// there to get access to default-visible members or to
							// unify types of conditional expressions.
				fUpdate.addCastToRemove(castCv);

			} else if (expressionVariable instanceof ArrayTypeVariable2 && castType.isArrayType()) { // bug 97258
				ArrayElementVariable2 arrayElementCv= fTCModel.getArrayElementVariable(expressionVariable);
				if (arrayElementCv == null)
					continue;
				TType chosenArrayElementType= InferTypeArgumentsConstraintsSolver.getChosenType(arrayElementCv);
				if (chosenArrayElementType != null && TTypes.canAssignTo(chosenArrayElementType, ((ArrayType) castType).getComponentType())) {
					if (expressionType instanceof ArrayType && chosenArrayElementType.equals(((ArrayType) expressionType).getComponentType()))
						continue; // The type has not changed. Don't remove the cast, since it could be
								// there to unify types of conditional expressions.
					fUpdate.addCastToRemove(castCv);
				}
			}
		}
	}

	public static TType getChosenType(ConstraintVariable2 cv) {
		TType type= (TType) cv.getData(CHOSEN_TYPE);
		if (type != null)
			return type;
		TypeEquivalenceSet set= cv.getTypeEquivalenceSet();
		if (set == null) { //TODO: should not have to set this here. Clean up when caching chosen type
			return null;
//			// no representative == no restriction
//			set= new TypeEquivalenceSet(cv);
//			set.setTypeEstimate(TypeUniverseSet.create());
//			cv.setTypeEquivalenceSet(set);
		}
		return cv.getTypeEstimate().chooseSingleType();
	}

	private static void setChosenType(ConstraintVariable2 cv, TType type) {
		cv.setData(CHOSEN_TYPE, type);
	}
}
