content: /*
   * Copyright 2019 the original author or authors.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.springframework.boot.graal.type;
  
  import java.lang.reflect.Modifier;
  import java.sql.Types;
  import java.util.AbstractMap;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Optional;
  import java.util.Set;
  import java.util.Stack;
  import java.util.stream.Collectors;
  
  import org.objectweb.asm.Opcodes;
  import org.objectweb.asm.signature.SignatureReader;
  import org.objectweb.asm.signature.SignatureVisitor;
  import org.objectweb.asm.tree.AnnotationNode;
  import org.objectweb.asm.tree.ClassNode;
  import org.objectweb.asm.tree.InnerClassNode;
  import org.objectweb.asm.tree.MethodNode;
  import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
  import org.springframework.context.annotation.ImportSelector;
  
  /**
   * @author Andy Clement
   */
  public class Type {
  	
  	// Spring Security
  	public final static String OAuth2ImportSelector = "Lorg/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector;";
  	
  	public final static String SpringWebMvcImportSelector = "Lorg/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector;";
  	
  	public final static String ImportSelector ="Lorg/springframework/context/annotation/ImportSelector;";
  	
  	public final static String TransactionManagementConfigurationSelector = "Lorg/springframework/transaction/annotation/TransactionManagementConfigurationSelector;";
  
  	public final static String SpringDataWebConfigurationSelector = "Lorg/springframework/data/web/config/EnableSpringDataWebSupport$SpringDataWebConfigurationImportSelector;";
  
  	public final static String SpringDataWebQueryDslSelector = "Lorg/springframework/data/web/config/EnableSpringDataWebSupport$QuerydslActivator;";
  
  	public final static String AdviceModeImportSelector="Lorg/springframework/context/annotation/AdviceModeImportSelector;";
  
  	public final static String EnableConfigurationPropertiesImportSelector = "Lorg/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector;";
  	
  	public final static String CacheConfigurationImportSelector = "Lorg/springframework/boot/autoconfigure/cache/CacheAutoConfiguration$CacheConfigurationImportSelector;";
  	
  	public final static String RabbitConfigurationImportSelector = "Lorg/springframework/amqp/rabbit/annotation/RabbitListenerConfigurationSelector;";
  	
  	public final static String AtBean = "Lorg/springframework/context/annotation/Bean;";
  
  	public final static String AtImports = "Lorg/springframework/context/annotation/Import;";
  
  	public final static String AtEnableConfigurationProperties = "Lorg/springframework/boot/context/properties/EnableConfigurationProperties;";
  	
  	public final static String AtConditionalOnClass = "Lorg/springframework/boot/autoconfigure/condition/ConditionalOnClass;";
  
  	public final static String AtConditional = "Lorg/springframework/context/annotation/Conditional;";
  
  	public final static String AtConditionalOnMissingBean = "Lorg/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean;";
  
  	public final static String HypermediaConfigurationImportSelector = "Lorg/springframework/hateoas/config/HypermediaConfigurationImportSelector;";
  
  	public final static String WebStackImportSelector = "Lorg/springframework/hateoas/config/WebStackImportSelector;";
  
  	public final static Type MISSING = new Type(null, null);
  
  	public final static Type[] NO_INTERFACES = new Type[0];
  
  	protected static Set<String> validBoxing = new HashSet<String>();
  
  
  	private TypeSystem typeSystem;
  
  	private ClassNode node;
  	
  	private Type[] interfaces;
  
  	public Type(TypeSystem typeSystem, ClassNode node) {
  		this.typeSystem = typeSystem;
  		this.node = node;
  	}
  
  	public static Type forClassNode(TypeSystem typeSystem, ClassNode node) {
  		return new Type(typeSystem, node);
  	}
  
  	/**
  	 * @return typename in slashed form (aaa/bbb/ccc/Ddd$Eee)
  	 */
  	public String getName() {
  		return node.name;
  	}
  
  	public String getDottedName() {
  		return node.name.replace("/", ".");
  	}
  
  	public Type getSuperclass() {
  		if (node.superName == null) {
  			return null;
  		}
  		return typeSystem.resolveSlashed(node.superName);
  	}
  	
  	@Override
  	public String toString() {
  		return "Type:"+getName();
  	}
  
  	public Type[] getInterfaces() {
  		if (interfaces == null) {
  			List<String> itfs = node.interfaces;
  			if (itfs.size() == 0) {
  				interfaces = NO_INTERFACES;
  			} else {
  				interfaces = new Type[itfs.size()];
  				for (int i = 0; i < itfs.size(); i++) {
  					interfaces[i] = typeSystem.resolveSlashed(itfs.get(i));
  				}
  			}
  		}
  		return interfaces;
  	}
  	
  	/** @return List of slashed interface types */
  	public List<String> getInterfacesStrings() {
  		return node.interfaces;
  	}
  
  	/** @return slashed supertype name */
  	public String getSuperclassString() {
  		return node.superName;
  	}
  	
  	public List<String> getTypesInSignature() {
  		if (node.signature == null) {
  			return Collections.emptyList();
  		} else {
  			SignatureReader reader = new SignatureReader(node.signature);
  			TypeCollector tc = new TypeCollector();
  			reader.accept(tc);
  			return tc.getTypes();
  		}
  	}
  	
  	static class TypeCollector extends SignatureVisitor {
  
  		List<String> types = null;
  		
  		public TypeCollector() {
  			super(Opcodes.ASM7);
  		}
  		
  		@Override
  		public void visitClassType(String name) {
  			if (types == null) {
  				types = new ArrayList<String>();
  			}
  			types.add(name);
  		}
  		
  		public List<String> getTypes() {
  			if (types == null) {
  				return Collections.emptyList();
  			} else {
  				return types;
  			}
  		}
  				
  	}
  
  	public boolean implementsInterface(String interfaceName) {
  		Type[] interfacesToCheck = getInterfaces();
  		for (Type interfaceToCheck : interfacesToCheck) {
  			if (interfaceToCheck.getName().equals(interfaceName)) {
  				return true;
  			}
  			if (interfaceToCheck.implementsInterface(interfaceName)) {
  				return true;
  			}
  		}
  		Type superclass = getSuperclass();
  		while (superclass != null) {
  			if (superclass.implementsInterface(interfaceName)) {
  				return true;
  			}
  			superclass = superclass.getSuperclass();
  		}
  		return false;
  	}
  
  	public List<Method> getMethodsWithAnnotation(String string) {
  		return node.methods.stream().filter(m -> hasAnnotation(m, string)).map(m -> wrap(m))
  				.collect(Collectors.toList());
  	}
  	
  	public List<Method> getMethodsWithAtBean() {
  		return getMethodsWithAnnotation(AtBean);
  	}
  
  	public Method wrap(MethodNode mn) {
  		return new Method(mn);
  	}
  
  	private boolean hasAnnotation(MethodNode m, String string) {
  		List<AnnotationNode> visibleAnnotations = m.visibleAnnotations;
  		Optional<AnnotationNode> findAny = visibleAnnotations == null ? Optional.empty()
  				: visibleAnnotations.stream().filter(a -> a.desc.equals(string)).findFirst();
  		return findAny.isPresent();
  	}
  
  	static {
  		validBoxing.add("Ljava/lang/Byte;B");
  		validBoxing.add("Ljava/lang/Character;C");
  		validBoxing.add("Ljava/lang/Double;D");
  		validBoxing.add("Ljava/lang/Float;F");
  		validBoxing.add("Ljava/lang/Integer;I");
  		validBoxing.add("Ljava/lang/Long;J");
  		validBoxing.add("Ljava/lang/Short;S");
  		validBoxing.add("Ljava/lang/Boolean;Z");
  		validBoxing.add("BLjava/lang/Byte;");
  		validBoxing.add("CLjava/lang/Character;");
  		validBoxing.add("DLjava/lang/Double;");
  		validBoxing.add("FLjava/lang/Float;");
  		validBoxing.add("ILjava/lang/Integer;");
  		validBoxing.add("JLjava/lang/Long;");
  		validBoxing.add("SLjava/lang/Short;");
  		validBoxing.add("ZLjava/lang/Boolean;");
  	}
  
  	public boolean isAssignableFrom(Type other) {
  		if (other.isPrimitiveType()) {
  			if (validBoxing.contains(this.getName() + other.getName())) {
  				return true;
  			}
  		}
  		if (this == other) {
  			return true;
  		}
  
  		if (this.getName().equals("Ljava/lang/Object;")) {
  			return true;
  		}
  
  //		if (!isTypeVariableReference()
  //				&& other.getSignature().equals("Ljava/lang/Object;")) {
  //			return false;
  //		}
  
  //		boolean thisRaw = this.isRawType();
  //		if (thisRaw && other.isParameterizedOrGenericType()) {
  //			return isAssignableFrom(other.getRawType());
  //		}
  //
  //		boolean thisGeneric = this.isGenericType();
  //		if (thisGeneric && other.isParameterizedOrRawType()) {
  //			return isAssignableFrom(other.getGenericType());
  //		}
  //
  //		if (this.isParameterizedType()) {
  //			// look at wildcards...
  //			if (((ReferenceType) this.getRawType()).isAssignableFrom(other)) {
  //				boolean wildcardsAllTheWay = true;
  //				ResolvedType[] myParameters = this.getResolvedTypeParameters();
  //				for (int i = 0; i < myParameters.length; i++) {
  //					if (!myParameters[i].isGenericWildcard()) {
  //						wildcardsAllTheWay = false;
  //					} else {
  //						BoundedReferenceType boundedRT = (BoundedReferenceType) myParameters[i];
  //						if (boundedRT.isExtends() || boundedRT.isSuper()) {
  //							wildcardsAllTheWay = false;
  //						}
  //					}
  //				}
  //				if (wildcardsAllTheWay && !other.isParameterizedType()) {
  //					return true;
  //				}
  //				// we have to match by parameters one at a time
  //				ResolvedType[] theirParameters = other
  //						.getResolvedTypeParameters();
  //				boolean parametersAssignable = true;
  //				if (myParameters.length == theirParameters.length) {
  //					for (int i = 0; i < myParameters.length
  //							&& parametersAssignable; i++) {
  //						if (myParameters[i] == theirParameters[i]) {
  //							continue;
  //						}
  //						// dont do this: pr253109
  //						// if
  //						// (myParameters[i].isAssignableFrom(theirParameters[i],
  //						// allowMissing)) {
  //						// continue;
  //						// }
  //						ResolvedType mp = myParameters[i];
  //						ResolvedType tp = theirParameters[i];
  //						if (mp.isParameterizedType()
  //								&& tp.isParameterizedType()) {
  //							if (mp.getGenericType().equals(tp.getGenericType())) {
  //								UnresolvedType[] mtps = mp.getTypeParameters();
  //								UnresolvedType[] ttps = tp.getTypeParameters();
  //								for (int ii = 0; ii < mtps.length; ii++) {
  //									if (mtps[ii].isTypeVariableReference()
  //											&& ttps[ii]
  //													.isTypeVariableReference()) {
  //										TypeVariable mtv = ((TypeVariableReferenceType) mtps[ii])
  //												.getTypeVariable();
  //										boolean b = mtv
  //												.canBeBoundTo((ResolvedType) ttps[ii]);
  //										if (!b) {// TODO incomplete testing here
  //													// I think
  //											parametersAssignable = false;
  //											break;
  //										}
  //									} else {
  //										parametersAssignable = false;
  //										break;
  //									}
  //								}
  //								continue;
  //							} else {
  //								parametersAssignable = false;
  //								break;
  //							}
  //						}
  //						if (myParameters[i].isTypeVariableReference()
  //								&& theirParameters[i].isTypeVariableReference()) {
  //							TypeVariable myTV = ((TypeVariableReferenceType) myParameters[i])
  //									.getTypeVariable();
  //							// TypeVariable theirTV =
  //							// ((TypeVariableReferenceType)
  //							// theirParameters[i]).getTypeVariable();
  //							boolean b = myTV.canBeBoundTo(theirParameters[i]);
  //							if (!b) {// TODO incomplete testing here I think
  //								parametersAssignable = false;
  //								break;
  //							} else {
  //								continue;
  //							}
  //						}
  //						if (!myParameters[i].isGenericWildcard()) {
  //							parametersAssignable = false;
  //							break;
  //						} else {
  //							BoundedReferenceType wildcardType = (BoundedReferenceType) myParameters[i];
  //							if (!wildcardType.alwaysMatches(theirParameters[i])) {
  //								parametersAssignable = false;
  //								break;
  //							}
  //						}
  //					}
  //				} else {
  //					parametersAssignable = false;
  //				}
  //				if (parametersAssignable) {
  //					return true;
  //				}
  //			}
  //		}
  //
  //		// eg this=T other=Ljava/lang/Object;
  //		if (isTypeVariableReference() && !other.isTypeVariableReference()) {
  //			TypeVariable aVar = ((TypeVariableReference) this)
  //					.getTypeVariable();
  //			return aVar.resolve(world).canBeBoundTo(other);
  //		}
  //
  //		if (other.isTypeVariableReference()) {
  //			TypeVariableReferenceType otherType = (TypeVariableReferenceType) other;
  //			if (this instanceof TypeVariableReference) {
  //				return ((TypeVariableReference) this)
  //						.getTypeVariable()
  //						.resolve(world)
  //						.canBeBoundTo(
  //								otherType.getTypeVariable().getFirstBound()
  //										.resolve(world));// pr171952
  //				// return
  //				// ((TypeVariableReference)this).getTypeVariable()==otherType
  //				// .getTypeVariable();
  //			} else {
  //				// FIXME asc should this say canBeBoundTo??
  //				return this.isAssignableFrom(otherType.getTypeVariable()
  //						.getFirstBound().resolve(world));
  //			}
  //		}
  
  		Type[] interfaces = other.getInterfaces();
  		for (Type intface : interfaces) {
  			boolean b;
  //			if (thisRaw && intface.isParameterizedOrGenericType()) {
  //				b = this.isAssignableFrom(intface.getRawType(), allowMissing);
  //			} else {
  			b = this.isAssignableFrom(intface);
  //			}
  			if (b) {
  				return true;
  			}
  		}
  		Type superclass = other.getSuperclass();
  		if (superclass != null) {
  			boolean b;
  //			if (thisRaw && superclass.isParameterizedOrGenericType()) {
  //				b = this.isAssignableFrom(superclass.getRawType(), allowMissing);
  //			} else {
  			b = this.isAssignableFrom(superclass);
  //			}
  			if (b) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  	private boolean isPrimitiveType() {
  		return false;
  	}
  
  	public boolean isInterface() {
  		return Modifier.isInterface(node.access);
  	}
  
  	public boolean hasAnnotationInHierarchy(String lookingFor) {
  		return hasAnnotationInHierarchy(lookingFor, new ArrayList<String>());
  	}
  
  	public boolean hasAnnotationInHierarchy(String lookingFor, List<String> seen) {
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode anno : node.visibleAnnotations) {
  				if (seen.contains(anno.desc))
  					continue;
  				seen.add(anno.desc);
  	//			System.out.println("Comparing "+anno.desc+" with "+lookingFor);
  				if (anno.desc.equals(lookingFor)) {
  					return true;
  				}
  				try {
  					Type resolve = typeSystem.Lresolve(anno.desc);
  					if (resolve.hasAnnotationInHierarchy(lookingFor, seen)) {
  						return true;
  					}
  				} catch (MissingTypeException mte) {
  					// not on classpath, that's ok
  				}
  			}
  		}
  		return false;
  	}
  
  	public boolean isMetaAnnotated(String slashedTypeDescriptor) {
  		return isMetaAnnotated(slashedTypeDescriptor, new HashSet<>());
  	}
  
  	public boolean isMetaAnnotated(String slashedTypeDescriptor, Set<String> seen) {
  //		System.out.println("Looking at "+this.getName()+" for "+slashedTypeDescriptor);
  		for (Type t : this.getAnnotations()) {
  			String tname = t.getName();
  			if (tname.equals(slashedTypeDescriptor)) {
  				return true;
  			}
  			if (!seen.contains(tname)) {
  				seen.add(tname);
  				if (t.isMetaAnnotated(slashedTypeDescriptor, seen)) {
  					return true;
  				}
  			}
  		}
  		return false;
  	}
  
  	List<Type> annotations = null;
  
  	public static final List<Type> NO_ANNOTATIONS = Collections.emptyList();
  
  	
  	public List<String> findConditionalOnMissingBeanValue() {
  		 List<String> findAnnotationValue = findAnnotationValue(AtConditionalOnMissingBean, false);
  		 if (findAnnotationValue==null) {
  			 if (node.visibleAnnotations != null) {
  					for (AnnotationNode an : node.visibleAnnotations) {
  						if (an.desc.equals(AtConditionalOnMissingBean)) {
  							System.out.println("??? found nothing on this @COC annotated thing "+this.getName());
  						}
  					}
  				}
  		 }
  		 return findAnnotationValue;
  	}
  
  	public List<String> findConditionalOnClassValue() {
  		 List<String> findAnnotationValue = findAnnotationValue(AtConditionalOnClass, false);
  		 if (findAnnotationValue==null) {
  			 if (node.visibleAnnotations != null) {
  					for (AnnotationNode an : node.visibleAnnotations) {
  						if (an.desc.equals(AtConditionalOnClass)) {
  							System.out.println("??? found nothing on this @COC annotated thing "+this.getName());
  						}
  					}
  				}
  		 }
  		 return findAnnotationValue;
  	}
  	
  	public List<String> findEnableConfigurationPropertiesValue() {
  		 List<String> values = findAnnotationValue(AtEnableConfigurationProperties, false);
  		 return values;
  	}
  
  	public Map<String,List<String>> findImports() {
  		 return findAnnotationValueWithHostAnnotation(AtImports, true, new HashSet<>());
  	}
  		
  	public List<String> findAnnotationValue(String annotationType, boolean searchMeta) {		
  		return findAnnotationValue(annotationType, searchMeta, new HashSet<>());
  	}
  	
  	@SuppressWarnings("unchecked")
  	public Map<String,List<String>> findAnnotationValueWithHostAnnotation(String annotationType, boolean searchMeta, Set<String> visited) {		
  		if (!visited.add(this.getName())) {
  			return Collections.emptyMap();
  		}
  		Map<String,List<String>> collectedResults = new LinkedHashMap<>();
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an : node.visibleAnnotations) {
  				if (an.desc.equals(annotationType)) {
  					List<Object> values = an.values;
  					if (values != null) {
  						for (int i=0;i<values.size();i+=2) {
  							if (values.get(i).equals("value")) {
  								 List<String> importedReferences = ((List<org.objectweb.asm.Type>)values.get(i+1))
  										.stream()
  										.map(t -> t.getDescriptor())
  										.collect(Collectors.toList());
  								collectedResults.put(this.getName().replace("/", "."), importedReferences);
  							}
  						}
  					}
  				}
  			}
  			if (searchMeta) {
  				for (AnnotationNode an: node.visibleAnnotations) {
  					// For example @EnableSomething might have @Import on it
  					Type annoType = null;
  					try {
  						annoType = typeSystem.Lresolve(an.desc);
  					} catch (MissingTypeException mte) { 
  						System.out.println("SBG: WARNING: Unable to find "+an.desc+" skipping...");
  						continue;
  					}
  					collectedResults.putAll(annoType.findAnnotationValueWithHostAnnotation(annotationType, searchMeta, visited));
  				}
  			}
  		}
  		return collectedResults;
  	}
  	
  
  	@SuppressWarnings("unchecked")
  	public List<String> findAnnotationValue(String annotationType, boolean searchMeta, Set<String> visited) {		
  		if (!visited.add(this.getName())) {
  			return Collections.emptyList();
  		}
  		List<String> collectedResults = new ArrayList<>();
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an : node.visibleAnnotations) {
  				if (an.desc.equals(annotationType)) {
  					List<Object> values = an.values;
  					if (values != null) {
  						for (int i=0;i<values.size();i+=2) {
  							if (values.get(i).equals("value")) {
  								return ( (List<org.objectweb.asm.Type>)values.get(i+1))
  										.stream()
  										.map(t -> t.getDescriptor())
  										.collect(Collectors.toCollection(() -> collectedResults));
  							}
  						}
  					}
  				}
  			}
  			if (searchMeta) {
  				for (AnnotationNode an: node.visibleAnnotations) {
  					// For example @EnableSomething might have @Import on it
  					Type annoType = typeSystem.Lresolve(an.desc);
  					collectedResults.addAll(annoType.findAnnotationValue(annotationType, searchMeta, visited));
  				}
  			}
  		}
  		return collectedResults;
  	}
  
  	private List<Type> getAnnotations() {
  		if (annotations == null) {
  			annotations = new ArrayList<>();
  			if (node.visibleAnnotations != null) {
  				for (AnnotationNode an : node.visibleAnnotations) {
  					try {
  						annotations.add(this.typeSystem.Lresolve(an.desc));
  					} catch (MissingTypeException mte) {
  						// that's ok you weren't relying on it anyway!
  					}
  				}
  			}
  //			if (node.invisibleAnnotations != null) {
  //			for (AnnotationNode an: node.invisibleAnnotations) {
  //				try {
  //					annotations.add(this.typeSystem.Lresolve(an.desc));
  //				} catch (MissingTypeException mte) {
  //					// that's ok you weren't relying on it anyway!
  //				}
  //			}
  //			}
  			if (annotations.size() == 0) {
  				annotations = NO_ANNOTATIONS;
  			}
  		}
  		return annotations;
  	}
  
  	public Type findAnnotation(Type searchType) {
  		List<Type> annos = getAnnotations();
  		for (Type anno : annos) {
  			if (anno.equals(searchType)) {
  				return anno;
  			}
  		}
  		return null;
  	}
  	
  	/**
  	 * @return true if meta annotated with org.springframework.stereotype.Indexed
  	 */
  	public Entry<String,String> isIndexedOrEntity() {
  		Type indexedType = isMetaAnnotated2("Lorg/springframework/stereotype/Indexed;");
  		if (indexedType != null) {
  			return new AbstractMap.SimpleImmutableEntry<String,String>(this.node.name.replace("/", "."),indexedType.getName().replace("/", "."));
  		} else {
  			indexedType = isMetaAnnotated2("Ljavax/persistence/Entity;");
  			if (indexedType != null) {
  				return new AbstractMap.SimpleImmutableEntry<String,String>(this.node.name.replace("/", "."),"javax.persistence.Entity");
  			}
  			Type t = isIndexedInHierarchy();
  			if ( t != null) {
  				// This should catch repositories where the Repository interface is marked @Indexed
  				//app.main.model.FooRepository=org.springframework.data.repository.Repository")
  				return new AbstractMap.SimpleImmutableEntry<String,String>(this.node.name.replace("/","."), t.node.name.replace("/","."));
  			}
  			return null;
  		}
  	}
  	
  	private Type isIndexedInHierarchy() {
  		if (isAnnotated("Lorg/springframework/stereotype/Indexed;")) {
  			return this;
  		}
  		Type[] is = getInterfaces();
  		for (Type i: is) {
  			Type t = i.isIndexedInHierarchy();
  			if (t != null) {
  				return t;
  			}
  		}
  		Type sc = getSuperclass();
  		if (sc!=null) {
  			return sc.isIndexedInHierarchy();
  		}
  		return null;
  	}
  // app.main.model.FooRepository=org.springframework.data.repository.Repository
  //		public List<String> findConditionalOnClassValue() {
  //			if (node.visibleAnnotations != null) {
  //				for (AnnotationNode an : node.visibleAnnotations) {
  //					if (an.desc.equals("Lorg/springframework/boot/autoconfigure/condition/ConditionalOnClass;")) {
  //						List<Object> values = an.values;
  //						for (int i=0;i<values.size();i+=2) {
  //							if (values.get(i).equals("value")) {
  //								return ( (List<org.objectweb.asm.Type>)values.get(i+1))
  //										.stream()
  //										.map(t -> t.getDescriptor())
  //										.collect(Collectors.toList());
  //							}
  //						}
  ////						for (Object o: values) {
  ////							System.out.println("<> "+o+"  "+(o==null?"":o.ge
  	
  	private Type isMetaAnnotated2(String Ldescriptor) {
  		return isMetaAnnotated2(Ldescriptor, new HashSet<>());
  	}
  
  	private boolean isAnnotated(String Ldescriptor) {
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an: node.visibleAnnotations) {
  				if (an.desc.equals(Ldescriptor)) {
  					return true;
  				}
  			}
  		}
  		return false;
  	}
  	
  	private Type isMetaAnnotated2(String Ldescriptor, Set<String> seen) {
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an: node.visibleAnnotations) {
  				if (seen.add(an.desc)) { 
  					if (an.desc.equals(Ldescriptor)) {
  						return this;//typeSystem.Lresolve(an.desc);
  					} else {
  						Type annoType = typeSystem.Lresolve(an.desc);
  						Type meta = annoType.isMetaAnnotated2(Ldescriptor, seen);
  						if (meta != null) {
  							return meta;
  						}
  					}
  				}
  			}
  		}
  		return null;
  	}
  
  	public List<Type> getNestedTypes() {
  		List<Type> result = null;
  		List<InnerClassNode> innerClasses = node.innerClasses;
  		for (InnerClassNode inner: innerClasses) {	
  			if (inner.outerName==null || !inner.outerName.equals(getName())) {
  //				System.out.println("SKIPPPING "+inner.name+" because outer is "+inner.outerName+" and we are looking at "+getName());
  				continue;
  			}
  			if (inner.name.equals(getName())) {
  				continue;
  			}
  			Type t = typeSystem.resolve(inner.name); // aaa/bbb/ccc/Ddd$Eee
  			if (result == null) {
  				result = new ArrayList<>();
  			}
  			result.add(t);
  		}
  		return result==null?Collections.emptyList():result;
  	}
  
  	public String getDescriptor() {
  		return "L"+node.name.replace(".", "/")+";";
  	}
  
  	/**
  	 * Find @CompilationHints directly on this type or used as a meta-annotation on annotations on this type.
  	 */
  	public Map<HintDescriptor, List<String>> getHints() {
  		Map<HintDescriptor, List<String>> hints = new LinkedHashMap<>();
  		CompilationHint hint = proposedAnnotations.get(getDescriptor());
  		if (hint !=null) {
  			List<Type> s = new ArrayList<>();
  			s.add(this);
  			hints.put(new HintDescriptor(s, hint.skipIfTypesMissing, hint.follow, hint.name), null);
  		}
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an: node.visibleAnnotations) {
  				Type annotationType = typeSystem.Lresolve(an.desc, true);
  				if (annotationType == null) {
  					System.out.println("Couldn't resolve "+an.desc);
  				} else {
  					Stack<Type> s = new Stack<>();
  					s.push(this);
  					annotationType.collectHints(an, hints, new HashSet<>(), s);
  				}
  			}
  		}
  		if (implementsImportSelector() && hints.size()==0) {
  			throw new IllegalStateException("No @CompilationHint found for import selector: "+getDottedName());
  		}
  		
  		return hints.size()==0? Collections.emptyMap():hints;
  	}
  	
  	// TODO repeatable annotations...
  	
  	private void collectHints(AnnotationNode an, Map<HintDescriptor, List<String>> hints, Set<AnnotationNode> visited, Stack<Type> annotationChain) {
  		if (!visited.add(an)) {
  			return;
  		}
  		try {
  			annotationChain.push(this);
  			// Am I a compilation hint?
  			CompilationHint hint = proposedAnnotations.get(an.desc);
  			if (hint !=null) {
  				hints.put(new HintDescriptor(new ArrayList<>(annotationChain), hint.skipIfTypesMissing, hint.follow, hint.name), collectTypes(an));
  			}
  			// check for meta annotation
  			if (node.visibleAnnotations != null) {
  					for (AnnotationNode an2: node.visibleAnnotations) {
  						Type annotationType = typeSystem.Lresolve(an2.desc, true);
  						if (annotationType == null) {
  							System.out.println("Couldn't resolve "+an2.desc);
  						} else {
  							annotationType.collectHints(an2, hints, visited, annotationChain);
  						}
  					}
  			}
  		} finally {
  			annotationChain.pop();
  		}
  	}
  
  	private CompilationHint findCompilationHintHelper(HashSet<Type> visited) {
  		if (!visited.add(this)) {
  			return null;
  		}
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an : node.visibleAnnotations) {
  				CompilationHint compilationHint = proposedAnnotations.get(an.desc);
  				if (compilationHint != null) {
  					return compilationHint;
  				}
  				Type resolvedAnnotation = typeSystem.Lresolve(an.desc);
  				compilationHint = resolvedAnnotation.findCompilationHintHelper(visited);
  				if (compilationHint != null) {
  					return compilationHint;
  				}
  			}
  		}
  		return null;
  	}
  
  	private List<String> collectTypes(AnnotationNode an) {
  		List<Object> values = an.values;
  		if (values != null) {
  			for (int i=0;i<values.size();i+=2) {
  				if (values.get(i).equals("value")) {
  					 List<String> importedReferences = ((List<org.objectweb.asm.Type>)values.get(i+1))
  							.stream()
  							.map(t -> t.getDescriptor())
  							.collect(Collectors.toList());
  					 return importedReferences;
  				}
  			}
  		}
  		return Collections.emptyList();
  	}
  
  	private static Map<String, CompilationHint> proposedAnnotations = new HashMap<>();
  	
  	static {
  		// @ConditionalOnClass has @CompilationHint(skipIfTypesMissing=true, follow=false)
  		proposedAnnotations.put(AtConditionalOnClass, new CompilationHint(true,false));
  		
  		// @ConditionalOnMissingBean has @CompilationHint(skipIfTypesMissing=true, follow=false)
  		proposedAnnotations.put(AtConditionalOnMissingBean, new CompilationHint(true, false));
  		
  		// TODO can be {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
  		// @Imports has @CompilationHint(skipIfTypesMissing=false?, follow=true)
  		proposedAnnotations.put(AtImports, new CompilationHint(false, true));
  		
  		// @Conditional has @CompilationHint(skipIfTypesMissing=false, follow=false)
  		proposedAnnotations.put(AtConditional, new CompilationHint(false, false));
  		
  		// TODO do configuration properties chain?
  		// @EnableConfigurationProperties has @CompilationHint(skipIfTypesMissing=false, follow=false)
  		proposedAnnotations.put(AtEnableConfigurationProperties, new CompilationHint(false, false));
  		
  		// @EnableConfigurationPropertiesImportSelector has
  		// @CompilationHint(skipIfTypesMissing=false, follow=false, name={
  		//   ConfigurationPropertiesBeanRegistrar.class.getName(),
  		//   ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() })
  		// proposedAnnotations.put(AtEnableConfigurationProperties, new CompilationHint(false, false));
  		
  		// CacheConfigurationImportSelector has
  		// @CompilationHint(skipIfTypesMissing=true, follow=false, name={
  		// 	"org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration",
  		//	 	"org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"})
  		proposedAnnotations.put(CacheConfigurationImportSelector,
  				new CompilationHint(false,false, new String[] {
  				 	"org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration",
  				 	"org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"}
  				));
  		
  		proposedAnnotations.put(RabbitConfigurationImportSelector,
  				new CompilationHint(true,true, new String[] {
  				 	"org.springframework.amqp.rabbit.annotation.RabbitBootstrapConfiguration"}
  				));
  		
  		//  TransactionManagementConfigurationSelector has
  		// @CompilationHint(skipIfTypesMissing=true, follow=true, name={...})
  		proposedAnnotations.put(TransactionManagementConfigurationSelector,
  				new CompilationHint(true, true, new String[] {
  					"org.springframework.context.annotation.AutoProxyRegistrar",
  					"org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration",
  					"org.springframework.transaction.aspectj.AspectJJtaTransactionManagementConfiguration",
  					"org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration"}
  				));
  		
  		//  EnableSpringDataWebSupport. TODO: there are others in spring.factories.
  		proposedAnnotations.put(SpringDataWebConfigurationSelector,
  				new CompilationHint(true, true, new String[] {
  					"org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration",
  					"org.springframework.data.web.config.SpringDataWebConfiguration"}
  				));
  		
  		//  EnableSpringDataWebSupport. TODO: there are others in spring.factories.
  		proposedAnnotations.put(SpringDataWebQueryDslSelector,
  				new CompilationHint(true, true, new String[] {
  					"org.springframework.data.web.config.QuerydslWebConfiguration"}
  				));
  		
  		// EnableConfigurationPropertiesImportSelector has
  		// @CompilationHint(skipIfTypesMissing=true, follow=false, name={
  		//	 	"org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector$ConfigurationPropertiesBeanRegistrar",
  		//	 	"org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar"})
  		proposedAnnotations.put(EnableConfigurationPropertiesImportSelector,
  				new CompilationHint(false,false, new String[] {
  				 	"org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector$ConfigurationPropertiesBeanRegistrar",
  				 	"org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar"}
  				));
  		
  				
  		// Not quite right... this is a superclass of a selector we've already added...
  		proposedAnnotations.put(AdviceModeImportSelector,
  				new CompilationHint(true, true, new String[0]
  				));
  
  		
  		// Spring Security!
  		proposedAnnotations.put(SpringWebMvcImportSelector,
  				new CompilationHint(false, true, new String[] {
  					"org.springframework.web.servlet.DispatcherServlet",
  					"org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration"
  				}));
  		proposedAnnotations.put(OAuth2ImportSelector,
  				new CompilationHint(false, true, new String[] {
  					"org.springframework.security.oauth2.client.registration.ClientRegistration",
  					"org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration"
  				}));
  
  		proposedAnnotations.put(HypermediaConfigurationImportSelector,
  				new CompilationHint(false, true, new String[] {
  						"org.springframework.hateoas.config.HypermediaConfigurationImportSelector"
  				}));
  
  		proposedAnnotations.put(WebStackImportSelector,
  				new CompilationHint(false, true, new String[] {
  						"org.springframework.hateoas.config.WebStackImportSelector"
  				}));
  	}
  	
  	private boolean implementsImportSelector() {
  		return implementsInterface(fromLdescriptorToSlashed(ImportSelector));
  	}
  	
  	private String fromLdescriptorToSlashed(String Ldescriptor) {
  		return Ldescriptor.substring(1,Ldescriptor.length()-1);
  	}
  	
  	private CompilationHint findCompilationHint(Type annotationType) {
  		String descriptor = "L"+annotationType.getName().replace(".", "/")+";";
  		CompilationHint hint = proposedAnnotations.get(descriptor);
  		if (hint !=null) {
  			return hint;
  		} else {
  			// check for meta annotation
  			return annotationType.findCompilationHintHelper(new HashSet<>());
  		}
  	}		
  	
  	// TODO what about AliasFor usage in spring annotations themselves? does that trip this code up when we start looking at particular fields?
  
  	static class CompilationHint {
  		boolean follow;
  		boolean skipIfTypesMissing;
  		private String[] name;
  		
  		public CompilationHint(boolean skipIfTypesMissing, boolean follow) {
  			this(skipIfTypesMissing,follow,new String[] {});
  		}
  		
  		// TODO what about whether you need to reflect on ctors/methods/fields?
  		public CompilationHint(boolean skipIfTypesMissing, boolean follow, String[] name) {
  			this.skipIfTypesMissing = skipIfTypesMissing;
  			this.follow = follow;
  			this.name = name;
  		}
  	}
  
  	public void collectMissingAnnotationTypesHelper(Set<String> missingAnnotationTypes, HashSet<Type> visited) {
  		if (!visited.add(this)) {
  			return;
  		}
  		if (node.visibleAnnotations != null) {
  			for (AnnotationNode an: node.visibleAnnotations) {
  				Type annotationType = typeSystem.Lresolve(an.desc, true);
  				if (annotationType == null) {
  					missingAnnotationTypes.add(an.desc.substring(0,an.desc.length()-1).replace("/", "."));
  				} else {
  					annotationType.collectMissingAnnotationTypesHelper(missingAnnotationTypes, visited);
  				}
  			}
  		}
  	}
  	
  //	@SuppressWarnings("unchecked")
  //	public void findCompilationHints(String annotationType, Map<String,List<String>> hintCollector, Set<String> visited) {		
  //		if (!visited.add(this.getName())) {
  //			return Collections.emptyMap();
  //		}
  //		Map<String,List<String>> collectedResults = new LinkedHashMap<>();
  //		if (node.visibleAnnotations != null) {
  //			for (AnnotationNode an : node.visibleAnnotations) {
  //				if (an.desc.equals(annotationType)) {
  //					List<Object> values = an.values;
  //					if (values != null) {
  //						for (int i=0;i<values.size();i+=2) {
  //							if (values.get(i).equals("value")) {
  //								 List<String> importedReferences = ((List<org.objectweb.asm.Type>)values.get(i+1))
  //										.stream()
  //										.map(t -> t.getDescriptor())
  //										.collect(Collectors.toList());
  //								collectedResults.put(this.getName().replace("/", "."), importedReferences);
  //							}
  //						}
  //					}
  //				}
  //			}
  //			if (searchMeta) {
  //				for (AnnotationNode an: node.visibleAnnotations) {
  //					// For example @EnableSomething might have @Import on it
  //					Type annoType = null;
  //					try {
  //						annoType = typeSystem.Lresolve(an.desc);
  //					} catch (MissingTypeException mte) { 
  //						System.out.println("SBG: WARNING: Unable to find "+an.desc+" skipping...");
  //						continue;
  //					}
  //					collectedResults.putAll(annoType.findCompilationHints(annotationType, visited));
  //				}
  //			}
  //		}
  //		return collectedResults;
  //	}
  
  	
  	// Assume @ConditionalOnClass has @CompilationHint(skipIfTypesMissing=true) and both value() and name() in
  	// the annotation would have @CompilationTypeList
  
  	
  
  }