/*******************************************************************************
 * 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.structure.constraints;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.ICompilationUnit;

import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.CastVariable2;
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.ITypeConstraintVariable;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ITypeSet;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ImmutableTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.TypeEquivalenceSet;

/**
 * Type constraint solver to solve supertype constraint models.
 *
 * @since 3.1
 */
public class SuperTypeConstraintsSolver {

	/** The type estimate data (type: <code>TType</code>) */
	public static final String DATA_TYPE_ESTIMATE= "te"; //$NON-NLS-1$

	/** The type constraint model to solve */
	protected final SuperTypeConstraintsModel fModel;

	/** The obsolete casts (element type: <code>&lt;ICompilationUnit, Collection&lt;CastVariable2&gt;&gt;</code>) */
	protected Map<ICompilationUnit, Collection<CastVariable2>> fObsoleteCasts= null;

	/** The list of constraint variables to be processed */
	protected LinkedList<ConstraintVariable2> fProcessable= null;

	/** The type occurrences (element type: <code>&lt;ICompilationUnit, Collection&lt;ITypeConstraintVariable&gt;</code>) */
	protected Map<ICompilationUnit, Collection<ITypeConstraintVariable>> fTypeOccurrences= null;

	/**
	 * Creates a new super type constraints solver.
	 *
	 * @param model the model to solve
	 */
	public SuperTypeConstraintsSolver(final SuperTypeConstraintsModel model) {
		Assert.isNotNull(model);

		fModel= model;
	}

	/**
	 * Computes the necessary equality constraints for conditional expressions.
	 *
	 * @param constraints the type constraints (element type: <code>ITypeConstraint2</code>)
	 * @param level the compliance level
	 */
	private void computeConditionalTypeConstraints(final Collection<ITypeConstraint2> constraints, final int level) {
		ITypeConstraint2 constraint= null;
		for (final Iterator<ITypeConstraint2> iterator= constraints.iterator(); iterator.hasNext();) {
			constraint= iterator.next();
			if (constraint instanceof ConditionalTypeConstraint) {
				final ConditionalTypeConstraint conditional= (ConditionalTypeConstraint) constraint;
				fModel.createEqualityConstraint(constraint.getLeft(), constraint.getRight());
				fModel.createEqualityConstraint(conditional.getExpression(), constraint.getLeft());
				fModel.createEqualityConstraint(conditional.getExpression(), constraint.getRight());
			}
		}
	}

	/**
	 * Computes the necessary equality constraints for non-covariant return types.
	 *
	 * @param constraints the type constraints (element type: <code>ITypeConstraint2</code>)
	 * @param level the compliance level
	 */
	private void computeNonCovariantConstraints(final Collection<ITypeConstraint2> constraints, final int level) {
		if (level != 3) {
			ITypeConstraint2 constraint= null;
			for (final Iterator<ITypeConstraint2> iterator= constraints.iterator(); iterator.hasNext();) {
				constraint= iterator.next();
				if (constraint instanceof CovariantTypeConstraint)
					fModel.createEqualityConstraint(constraint.getLeft(), constraint.getRight());
			}
		}
	}

	/**
	 * Computes the obsolete casts for the specified cast variables.
	 *
	 * @param variables the cast variables (element type: <code>CastVariable2</code>)
	 */
	private void computeObsoleteCasts(final Collection<CastVariable2> variables) {
		fObsoleteCasts= new HashMap<>();
		CastVariable2 variable= null;
		for (final Iterator<CastVariable2> iterator= variables.iterator(); iterator.hasNext();) {
			variable= iterator.next();
			final TType type= (TType) variable.getExpressionVariable().getData(DATA_TYPE_ESTIMATE);
			if (type != null && type.canAssignTo(variable.getType())) {
				final ICompilationUnit unit= variable.getCompilationUnit();
				Collection<CastVariable2> casts= fObsoleteCasts.get(unit);
				if (casts != null)
					casts.add(variable);
				else {
					casts= new ArrayList<>(1);
					casts.add(variable);
					fObsoleteCasts.put(unit, casts);
				}
			}
		}
	}

	/**
	 * Computes the initial type estimate for the specified constraint variable.
	 *
	 * @param variable the constraint variable
	 * @return the initial type estimate
	 */
	protected ITypeSet computeTypeEstimate(final ConstraintVariable2 variable) {
		final TType type= variable.getType();
		if (variable instanceof ImmutableTypeVariable2 || !type.getErasure().equals(fModel.getSubType().getErasure()))
			return SuperTypeSet.createTypeSet(type);
		return SuperTypeSet.createTypeSet(type, fModel.getSuperType());
	}

	/**
	 * Computes the initial type estimates for the specified variables.
	 *
	 * @param variables the constraint variables (element type: <code>ConstraintVariable2</code>)
	 */
	private void computeTypeEstimates(final Collection<ConstraintVariable2> variables) {
		ConstraintVariable2 variable= null;
		for (final Iterator<ConstraintVariable2> iterator= variables.iterator(); iterator.hasNext();) {
			variable= iterator.next();
			TypeEquivalenceSet set= variable.getTypeEquivalenceSet();
			if (set == null) {
				set= new TypeEquivalenceSet(variable);
				set.setTypeEstimate(computeTypeEstimate(variable));
				variable.setTypeEquivalenceSet(set);
			} else {
				ITypeSet estimate= variable.getTypeEstimate();
				if (estimate == null) {
					estimate= SuperTypeSet.getUniverse();
					for (ConstraintVariable2 v : set.getContributingVariables()) {
						estimate= estimate.restrictedTo(computeTypeEstimate(v));
					}
					set.setTypeEstimate(estimate);
				}
			}
		}
	}

	/**
	 * Computes a single type for each of the specified constraint variables.
	 *
	 * @param variables the constraint variables (element type: <code>ConstraintVariable2</code>)
	 */
	private void computeTypeOccurrences(final Collection<ConstraintVariable2> variables) {
		fTypeOccurrences= new HashMap<>();
		final TType superErasure= fModel.getSuperType().getErasure();
		TType estimatedType= null;
		ITypeSet set= null;
		ICompilationUnit unit= null;
		ConstraintVariable2 variable= null;
		ITypeConstraintVariable declaration= null;
		TType variableType= null;
		for (final Iterator<ConstraintVariable2> iterator= variables.iterator(); iterator.hasNext();) {
			variable= iterator.next();
			if (variable instanceof ITypeConstraintVariable) {
				declaration= (ITypeConstraintVariable) variable;
				variableType= variable.getType();
				set= declaration.getTypeEstimate();
				if (set != null) {
					estimatedType= set.chooseSingleType();
					if (estimatedType != null) {
						final TType typeErasure= estimatedType.getErasure();
						if (!typeErasure.equals(variableType.getErasure()) && typeErasure.equals(superErasure)) {
							declaration.setData(DATA_TYPE_ESTIMATE, estimatedType);
							unit= declaration.getCompilationUnit();
							if (unit != null) {
								Collection<ITypeConstraintVariable> matches= fTypeOccurrences.get(unit);
								if (matches != null)
									matches.add(declaration);
								else {
									matches= new ArrayList<>(1);
									matches.add(declaration);
									fTypeOccurrences.put(unit, matches);
								}
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Returns the computed obsolete casts.
	 *
	 * @return the obsolete casts (element type: <code>&lt;ICompilationUnit, Collection&lt;CastVariable2&gt;&gt;</code>)
	 */
	public final Map<ICompilationUnit, Collection<CastVariable2>> getObsoleteCasts() {
		return fObsoleteCasts;
	}

	/**
	 * Returns the computed type occurrences.
	 *
	 * @return the type occurrences (element type: <code>&lt;ICompilationUnit, Collection&lt;IDeclaredConstraintVariable&gt;</code>)
	 */
	public final Map<ICompilationUnit, Collection<ITypeConstraintVariable>> getTypeOccurrences() {
		return fTypeOccurrences;
	}

	/**
	 * Processes the given constraints on the constraint variable and propagates it.
	 *
	 * @param constraints the type constraints to process (element type: <code>ITypeConstraint2</code>)
	 */
	private void processConstraints(final Collection<ITypeConstraint2> constraints) {
		final int level= fModel.getCompliance();
		ITypeConstraint2 constraint= null;
		for (final Iterator<ITypeConstraint2> iterator= constraints.iterator(); iterator.hasNext();) {
			constraint= iterator.next();
			if ((level == 3 || !(constraint instanceof CovariantTypeConstraint)) && !(constraint instanceof ConditionalTypeConstraint)) {
				final ConstraintVariable2 leftVariable= constraint.getLeft();
				final ITypeSet leftEstimate= leftVariable.getTypeEstimate();
				final TypeEquivalenceSet set= leftVariable.getTypeEquivalenceSet();
				final ITypeSet newEstimate= leftEstimate.restrictedTo(constraint.getRight().getTypeEstimate());
				if (leftEstimate != newEstimate) {
					set.setTypeEstimate(newEstimate);
					fProcessable.addAll(Arrays.asList(set.getContributingVariables()));
				}
			}
		}
	}

	/**
	 * Solves the constraints of the associated model.
	 */
	public final void solveConstraints() {
		fProcessable= new LinkedList<>();
		final Collection<ConstraintVariable2> variables= fModel.getConstraintVariables();
		final Collection<ITypeConstraint2> constraints= fModel.getTypeConstraints();
		final int level= fModel.getCompliance();
		computeNonCovariantConstraints(constraints, level);

		// TODO: use most specific common type for AST.JLS3
		computeConditionalTypeConstraints(constraints, level);

		computeTypeEstimates(variables);
		fProcessable.addAll(variables);
		Collection<ITypeConstraint2> usage= null;
		ConstraintVariable2 variable= null;
		while (!fProcessable.isEmpty()) {
			variable= fProcessable.removeFirst();
			usage= SuperTypeConstraintsModel.getVariableUsage(variable);
			if (!usage.isEmpty())
				processConstraints(usage);
			else
				variable.setData(DATA_TYPE_ESTIMATE, variable.getTypeEstimate().chooseSingleType());
		}
		computeTypeOccurrences(variables);
		computeObsoleteCasts(fModel.getCastVariables());
	}
}
