content: package io.activej.types;
  
  import java.lang.reflect.GenericArrayType;
  import java.lang.reflect.Type;
  import java.lang.reflect.TypeVariable;
  import java.lang.reflect.WildcardType;
  import java.util.Map;
  
  import static io.activej.types.Types.*;
  
  /**
   * Various utility methods for type inference
   */
  public class IsAssignableUtils {
  
  	/**
  	 * Tests whether a {@code from} type is assignable to {@code to} type
  	 *
  	 * @param to   a 'to' type that should be checked for possible assignment
  	 * @param from a 'from' type that should be checked for possible assignment
  	 * @return whether an object of type {@code from} is assignable to an object of type {@code to}
  	 */
  	public static boolean isAssignable(Type to, Type from) {
  		// shortcut
  		if (to instanceof Class && from instanceof Class) return ((Class<?>) to).isAssignableFrom((Class<?>) from);
  		return isAssignable(to, from, false);
  	}
  
  	private static boolean isAssignable(Type to, Type from, boolean strict) {
  		if (to instanceof WildcardType || from instanceof WildcardType) {
  			Type[] toUppers, toLowers;
  			if (to instanceof WildcardType wildcardTo) {
  				toUppers = wildcardTo.getUpperBounds();
  				toLowers = wildcardTo.getLowerBounds();
  			} else {
  				toUppers = new Type[]{to};
  				toLowers = strict ? toUppers : NO_TYPES;
  			}
  
  			Type[] fromUppers, fromLowers;
  			if (from instanceof WildcardType wildcardTo) {
  				fromUppers = wildcardTo.getUpperBounds();
  				fromLowers = wildcardTo.getLowerBounds();
  			} else {
  				fromUppers = new Type[]{from};
  				fromLowers = strict ? fromUppers : NO_TYPES;
  			}
  
  			for (Type toUpper : toUppers) {
  				for (Type fromUpper : fromUppers) {
  					if (!isAssignable(toUpper, fromUpper, false)) return false;
  				}
  			}
  			if (toLowers.length == 0) return true;
  			if (fromLowers.length == 0) return false;
  			for (Type toLower : toLowers) {
  				for (Type fromLower : fromLowers) {
  					if (!isAssignable(fromLower, toLower, false)) return false;
  				}
  			}
  			return true;
  		}
  		if (to instanceof GenericArrayType) to = getRawType(to);
  		if (from instanceof GenericArrayType) from = getRawType(from);
  		if (!strict && to instanceof Class) {
  			return ((Class<?>) to).isAssignableFrom(getRawType(from));
  		}
  		Class<?> toRawClazz = getRawType(to);
  		Type[] toTypeArguments = getActualTypeArguments(to);
  		return isAssignable(toRawClazz, toTypeArguments, from, strict);
  	}
  
  	private static boolean isAssignable(Class<?> toRawClazz, Type[] toTypeArguments, Type from, boolean strict) {
  		Class<?> fromRawClazz = getRawType(from);
  		if (strict && !toRawClazz.equals(fromRawClazz)) return false;
  		if (!strict && !toRawClazz.isAssignableFrom(fromRawClazz)) return false;
  		if (toRawClazz.isArray()) return true;
  		Type[] fromTypeArguments = getActualTypeArguments(from);
  		if (toRawClazz == fromRawClazz) {
  			if (toTypeArguments.length > fromTypeArguments.length) return false;
  			for (int i = 0; i < toTypeArguments.length; i++) {
  				if (!isAssignable(toTypeArguments[i], fromTypeArguments[i], true)) return false;
  			}
  			return true;
  		}
  		Map<TypeVariable<?>, Type> typeBindings = getTypeBindings(from);
  		for (Type anInterface : fromRawClazz.getGenericInterfaces()) {
  			if (isAssignable(toRawClazz, toTypeArguments, bind(anInterface, key -> typeBindings.getOrDefault(key, Types.wildcardTypeAny())), false)) {
  				return true;
  			}
  		}
  		Type superclass = fromRawClazz.getGenericSuperclass();
  		return superclass != null && isAssignable(toRawClazz, toTypeArguments, bind(superclass, typeBindings), false);
  	}
  
  }
  