/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *     Dmitry Stalnov (dstalnov@fusionone.com) - contributed fix for
 *       bug "inline method - doesn't handle implicit cast" (see
 *       https://bugs.eclipse.org/bugs/show_bug.cgi?id=24941).
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.dom;

import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.PrimitiveType;

import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TypeEnvironment;

/**
 * Helper class to check if objects are assignable to each other.
 * Methods with multiple arguments also work across bindings environments.
 */
// @see JDTUIHelperClasses
public class TypeRules {

	/**
	 * Tests if two types are assign compatible. Void types are never compatible.
	 *
	 * @param typeToAssign The binding of the type to assign
	 * @param definedType The type of the object that is assigned
	 * @return <code>true</code> iff definedType = typeToAssign is a valid assignment
	 */
	public static boolean canAssign(ITypeBinding typeToAssign, ITypeBinding definedType) {
		TypeEnvironment typeEnvironment= new TypeEnvironment(false, true);
		TType defined= typeEnvironment.create(definedType);
		TType toAssign= typeEnvironment.create(typeToAssign);
		return toAssign.canAssignTo(defined);
	}

	public static boolean isArrayCompatible(ITypeBinding definedType) {
		if (definedType.isTopLevel()) {
			if (definedType.isClass()) {
				return "Object".equals(definedType.getName()) && "java.lang".equals(definedType.getPackage().getName());  //$NON-NLS-1$//$NON-NLS-2$
			} else {
				String qualifiedName= definedType.getQualifiedName();
				return "java.io.Serializable".equals(qualifiedName) || "java.lang.Cloneable".equals(qualifiedName); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return false;
	}

	public static boolean isJavaLangObject(ITypeBinding definedType) {
		return definedType.isTopLevel() && definedType.isClass() && "Object".equals(definedType.getName()) && "java.lang".equals(definedType.getPackage().getName());  //$NON-NLS-1$//$NON-NLS-2$
	}

	/**
	 * Tests if a two types are cast compatible
	 * @param castType The binding of the type to cast to
	 * @param bindingToCast The binding ef the expression to cast.
	 * @return boolean Returns true if (castType) bindingToCast is a valid cast expression (can be unnecessary, but not invalid).
	 */
	public static boolean canCast(ITypeBinding castType, ITypeBinding bindingToCast) {
		//see bug 80715

		String voidName= PrimitiveType.VOID.toString();

		if (castType.isAnonymous() || castType.isNullType() || voidName.equals(castType.getName())) {
			throw new IllegalArgumentException();
		}

		if (castType == bindingToCast) {
			return true;
		}

		if (voidName.equals(bindingToCast.getName())) {
			return false;
		}

		if (bindingToCast.isArray()) {
			if (!castType.isArray()) {
				return isArrayCompatible(castType); // can not cast an arraytype to a non array type (except to Object, Serializable...)
			}

			int toCastDim= bindingToCast.getDimensions();
			int castTypeDim= castType.getDimensions();
			if (toCastDim == castTypeDim) {
				bindingToCast= bindingToCast.getElementType();
				castType= castType.getElementType();
				if (castType.isPrimitive() && castType != bindingToCast) {
					return false; // can't assign arrays of different primitive types to each other
				}
				// fall through
			} else if (toCastDim < castTypeDim) {
				return isArrayCompatible(bindingToCast.getElementType());
			} else {
				return isArrayCompatible(castType.getElementType());
			}
		}
		if (castType.isPrimitive()) {
			if (!bindingToCast.isPrimitive()) {
				return false;
			}
			String boolName= PrimitiveType.BOOLEAN.toString();
			return (!boolName.equals(castType.getName()) && !boolName.equals(bindingToCast.getName()));
		} else {
			if (bindingToCast.isPrimitive()) {
				return false;
			}
			if (castType.isArray()) {
				return isArrayCompatible(bindingToCast);
			}
			if (castType.isInterface()) {
				if ((bindingToCast.getModifiers() & Modifier.FINAL) != 0) {
					return Bindings.isSuperType(castType, bindingToCast);
				} else {
					return true;
				}
			}
			if (bindingToCast.isInterface()) {
				if ((castType.getModifiers() & Modifier.FINAL) != 0) {
					return Bindings.isSuperType(bindingToCast, castType);
				} else {
					return true;
				}
			}
			if (isJavaLangObject(castType)) {
				return true;
			}

			return Bindings.isSuperType(bindingToCast, castType) || Bindings.isSuperType(castType, bindingToCast);
		}
	}

	private TypeRules() {
	}

}
