/* *******************************************************************
 * Copyright (c) 2005, 2017 Contributors.
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 * ******************************************************************/
package org.aspectj.weaver.reflect;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;

import org.aspectj.apache.bcel.classfile.AnnotationDefault;
import org.aspectj.apache.bcel.classfile.Attribute;
import org.aspectj.apache.bcel.classfile.JavaClass;
import org.aspectj.apache.bcel.classfile.LocalVariable;
import org.aspectj.apache.bcel.classfile.LocalVariableTable;
import org.aspectj.apache.bcel.classfile.annotation.AnnotationGen;
import org.aspectj.apache.bcel.util.ClassLoaderRepository;
import org.aspectj.apache.bcel.util.NonCachingClassLoaderRepository;
import org.aspectj.apache.bcel.util.Repository;
import org.aspectj.weaver.AnnotationAJ;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.UnresolvedType;
import org.aspectj.weaver.World;
import org.aspectj.weaver.bcel.BcelAnnotation;
import org.aspectj.weaver.bcel.BcelWeakClassLoaderReference;

/**
 *
 * @author Adrian Colyer
 * @author Andy Clement
 */
public class Java15AnnotationFinder implements AnnotationFinder, ArgNameFinder {

	public static final ResolvedType[][] NO_PARAMETER_ANNOTATIONS = new ResolvedType[][] {};

	private Repository bcelRepository;
	private BcelWeakClassLoaderReference classLoaderRef;

	private static Repository staticBcelRepository;
	private static BcelWeakClassLoaderReference staticClassLoaderRef;

	private World world;
	private static boolean useCachingClassLoaderRepository;

	// Use single instance of Repository and ClassLoader
	public static boolean useSingleInstances =
		System.getProperty("org.aspectj.apache.bcel.useSingleRepositoryInstance", "false").equalsIgnoreCase("true");

	static {
		try {
			useCachingClassLoaderRepository = System.getProperty("Xset:bcelRepositoryCaching","true").equalsIgnoreCase("true");
		} catch (Throwable t) {
			useCachingClassLoaderRepository = false;
		}
	}

	// must have no-arg constructor for reflective construction
	public Java15AnnotationFinder() {
	}

	public void setClassLoader(ClassLoader aLoader) {
		// Set class loader ref
		if (useSingleInstances && staticClassLoaderRef == null) {
			staticClassLoaderRef = new BcelWeakClassLoaderReference(aLoader);
		} else {
			this.classLoaderRef = new BcelWeakClassLoaderReference(aLoader);
		}

		// Set repository
		if (useCachingClassLoaderRepository) {
			if (useSingleInstances && staticBcelRepository == null) {
				staticBcelRepository = new ClassLoaderRepository(getClassLoader());
			} else {
				this.bcelRepository = new ClassLoaderRepository(classLoaderRef);
			}
		} else {
			if (useSingleInstances && staticBcelRepository == null) {
				staticBcelRepository = new NonCachingClassLoaderRepository(getClassLoader());
			} else {
				this.bcelRepository = new NonCachingClassLoaderRepository(classLoaderRef);
			}
		}
	}

	private Repository getBcelRepository() {
		return useSingleInstances ? staticBcelRepository : bcelRepository;
	}

	public void setWorld(World aWorld) {
		this.world = aWorld;
	}

	public Object getAnnotation(ResolvedType annotationType, Object onObject) {
		try {
			Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) Class.forName(annotationType.getName(),
					false, getClassLoader());
			if (onObject.getClass().isAnnotationPresent(annotationClass)) {
				return onObject.getClass().getAnnotation(annotationClass);
			}
		} catch (ClassNotFoundException ex) {
			// just return null
		}
		return null;
	}

	public Object getAnnotationFromClass(ResolvedType annotationType, Class aClass) {
		try {
			Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) Class.forName(annotationType.getName(),
					false, getClassLoader());
			if (aClass.isAnnotationPresent(annotationClass)) {
				return aClass.getAnnotation(annotationClass);
			}
		} catch (ClassNotFoundException ex) {
			// just return null
		}
		return null;
	}

	public Object getAnnotationFromMember(ResolvedType annotationType, Member aMember) {
		if (!(aMember instanceof AccessibleObject))
			return null;
		AccessibleObject ao = (AccessibleObject) aMember;
		try {
			Class annotationClass = Class.forName(annotationType.getName(), false, getClassLoader());
			if (ao.isAnnotationPresent(annotationClass)) {
				return ao.getAnnotation(annotationClass);
			}
		} catch (ClassNotFoundException ex) {
			// just return null
		}
		return null;
	}

	private ClassLoader getClassLoader() {
		return useSingleInstances ? staticClassLoaderRef.getClassLoader() : classLoaderRef.getClassLoader();
	}

	public AnnotationAJ getAnnotationOfType(UnresolvedType ofType, Member onMember) {
		if (!(onMember instanceof AccessibleObject))
			return null;
		// here we really want both the runtime visible AND the class visible
		// annotations
		// so we bail out to Bcel and then chuck away the JavaClass so that we
		// don't hog
		// memory.
		try {
			JavaClass jc = bcelRepository.loadClass(onMember.getDeclaringClass());
			org.aspectj.apache.bcel.classfile.annotation.AnnotationGen[] anns = AnnotationGen.NO_ANNOTATIONS;
			if (onMember instanceof Method) {
				org.aspectj.apache.bcel.classfile.Method bcelMethod = jc.getMethod((Method) onMember);
				if (bcelMethod == null) {
					// pr220430
					// System.err.println(
					// "Unexpected problem in Java15AnnotationFinder: cannot retrieve annotations on method '"
					// +
					// onMember.getName()+"' in class '"+jc.getClassName()+"'");
				} else {
					anns = bcelMethod.getAnnotations();
				}
			} else if (onMember instanceof Constructor) {
				org.aspectj.apache.bcel.classfile.Method bcelCons = jc.getMethod((Constructor<?>) onMember);
				anns = bcelCons.getAnnotations();
			} else if (onMember instanceof Field) {
				org.aspectj.apache.bcel.classfile.Field bcelField = jc.getField((Field) onMember);
				anns = bcelField.getAnnotations();
			}
			// the answer is cached and we don't want to hold on to memory
			getBcelRepository().clear();
			// OPTIMIZE make constant 0 size array for sharing
			if (anns == null)
				anns = AnnotationGen.NO_ANNOTATIONS;
			// convert to our Annotation type
			for (org.aspectj.apache.bcel.classfile.annotation.AnnotationGen ann : anns) {
				if (ann.getTypeSignature().equals(ofType.getSignature())) {
					return new BcelAnnotation(ann, world);
				}
			}
			return null;
		} catch (ClassNotFoundException cnfEx) {
			// just use reflection then
		}
		return null;
	}

	public String getAnnotationDefaultValue(Member onMember) {
		try {
			JavaClass jc = getBcelRepository().loadClass(onMember.getDeclaringClass());
			if (onMember instanceof Method) {
				org.aspectj.apache.bcel.classfile.Method bcelMethod = jc.getMethod((Method) onMember);

				if (bcelMethod == null) {
					// pr220430
					// System.err.println(
					// "Unexpected problem in Java15AnnotationFinder: cannot retrieve annotations on method '"
					// +
					// onMember.getName()+"' in class '"+jc.getClassName()+"'");
				} else {
					Attribute[] attrs = bcelMethod.getAttributes();
					for (Attribute attribute : attrs) {
						if (attribute.getName().equals("AnnotationDefault")) {
							AnnotationDefault def = (AnnotationDefault) attribute;
							return def.getElementValue().stringifyValue();
						}
					}
					return null;
				}
			}
		} catch (ClassNotFoundException cnfEx) {
			// just use reflection then
		}
		return null;
	}

	public ResolvedType[] getAnnotations(Member onMember, boolean areRuntimeAnnotationsSufficient) {
		if (!(onMember instanceof AccessibleObject)) {
			return ResolvedType.NONE;
		}
		// If annotations with class level retention are required then we need to open
		// open the class file. If only runtime retention annotations are required
		// we can just use reflection.
		if (!areRuntimeAnnotationsSufficient) {
			try {
				JavaClass jc = getBcelRepository().loadClass(onMember.getDeclaringClass());
				org.aspectj.apache.bcel.classfile.annotation.AnnotationGen[] anns = null;
				if (onMember instanceof Method) {
					org.aspectj.apache.bcel.classfile.Method bcelMethod = jc.getMethod((Method) onMember);
					if (bcelMethod != null) {
						anns = bcelMethod.getAnnotations();
					}
				} else if (onMember instanceof Constructor) {
					org.aspectj.apache.bcel.classfile.Method bcelCons = jc.getMethod((Constructor<?>) onMember);
					anns = bcelCons.getAnnotations();
				} else if (onMember instanceof Field) {
					org.aspectj.apache.bcel.classfile.Field bcelField = jc.getField((Field) onMember);
					anns = bcelField.getAnnotations();
				}
				// the answer is cached and we don't want to hold on to memory
				getBcelRepository().clear();
				if (anns == null || anns.length == 0) {
					return ResolvedType.NONE;
				}
				ResolvedType[] annotationTypes = new ResolvedType[anns.length];
				for (int i = 0; i < anns.length; i++) {
					annotationTypes[i] = world.resolve(UnresolvedType.forSignature(anns[i].getTypeSignature()));
				}
				return annotationTypes;
			} catch (ClassNotFoundException cnfEx) {
				// just use reflection then
			}
		}

		AccessibleObject ao = (AccessibleObject) onMember;
		Annotation[] anns = ao.getDeclaredAnnotations();
		if (anns.length == 0) {
			return ResolvedType.NONE;
		}
		ResolvedType[] annotationTypes = new ResolvedType[anns.length];
		for (int i = 0; i < anns.length; i++) {
			annotationTypes[i] = UnresolvedType.forName(anns[i].annotationType().getName()).resolve(world);
		}
		return annotationTypes;
	}

	public ResolvedType[] getAnnotations(Class forClass, World inWorld) {
		// here we really want both the runtime visible AND the class visible
		// annotations so we bail out to Bcel and then chuck away the JavaClass so that we
		// don't hog memory.
		try {
			JavaClass jc = getBcelRepository().loadClass(forClass);
			org.aspectj.apache.bcel.classfile.annotation.AnnotationGen[] anns = jc.getAnnotations();
			getBcelRepository().clear();
			if (anns == null) {
				return ResolvedType.NONE;
			} else {
				ResolvedType[] ret = new ResolvedType[anns.length];
				for (int i = 0; i < ret.length; i++) {
					ret[i] = inWorld.resolve(UnresolvedType.forSignature(anns[i].getTypeSignature()));
				}
				return ret;
			}
		} catch (ClassNotFoundException cnfEx) {
			// just use reflection then
		}

		Annotation[] classAnnotations = forClass.getAnnotations();
		ResolvedType[] ret = new ResolvedType[classAnnotations.length];
		for (int i = 0; i < classAnnotations.length; i++) {
			ret[i] = inWorld.resolve(classAnnotations[i].annotationType().getName());
		}

		return ret;
	}

	public String[] getParameterNames(Member forMember) {
		if (!(forMember instanceof AccessibleObject))
			return null;

		try {
			JavaClass jc = getBcelRepository().loadClass(forMember.getDeclaringClass());
			LocalVariableTable lvt = null;
			int numVars = 0;
			if (forMember instanceof Method) {
				org.aspectj.apache.bcel.classfile.Method bcelMethod = jc.getMethod((Method) forMember);
				lvt = bcelMethod.getLocalVariableTable();
				numVars = bcelMethod.getArgumentTypes().length;
			} else if (forMember instanceof Constructor) {
				org.aspectj.apache.bcel.classfile.Method bcelCons = jc.getMethod((Constructor<?>) forMember);
				lvt = bcelCons.getLocalVariableTable();
				numVars = bcelCons.getArgumentTypes().length;
			}
			return getParameterNamesFromLVT(lvt, numVars);
		} catch (ClassNotFoundException cnfEx) {
			; // no luck
		}

		return null;
	}

	private String[] getParameterNamesFromLVT(LocalVariableTable lvt, int numVars) {
		if (lvt == null)
			return null;// pr222987 - prevent NPE
		LocalVariable[] vars = lvt.getLocalVariableTable();
		if (vars.length < numVars) {
			// basic error, we can't get the names...
			return null;
		}
		String[] ret = new String[numVars];
		for (int i = 0; i < numVars; i++) {
			ret[i] = vars[i + 1].getName();
		}
		return ret;
	}

	public ResolvedType[][] getParameterAnnotationTypes(Member onMember) {
		if (!(onMember instanceof AccessibleObject))
			return NO_PARAMETER_ANNOTATIONS;
		// here we really want both the runtime visible AND the class visible
		// annotations
		// so we bail out to Bcel and then chuck away the JavaClass so that we
		// don't hog
		// memory.
		try {
			JavaClass jc = getBcelRepository().loadClass(onMember.getDeclaringClass());
			org.aspectj.apache.bcel.classfile.annotation.AnnotationGen[][] anns = null;
			if (onMember instanceof Method) {
				org.aspectj.apache.bcel.classfile.Method bcelMethod = jc.getMethod((Method) onMember);
				if (bcelMethod == null) {
					// pr220430
					// System.err.println(
					// "Unexpected problem in Java15AnnotationFinder: cannot retrieve annotations on method '"
					// +
					// onMember.getName()+"' in class '"+jc.getClassName()+"'");
				} else {
					anns = bcelMethod.getParameterAnnotations();
				}
			} else if (onMember instanceof Constructor) {
				org.aspectj.apache.bcel.classfile.Method bcelCons = jc.getMethod((Constructor<?>) onMember);
				anns = bcelCons.getParameterAnnotations();
			} else if (onMember instanceof Field) {
				// anns = null;
			}
			// the answer is cached and we don't want to hold on to memory
			getBcelRepository().clear();
			if (anns == null)
				return NO_PARAMETER_ANNOTATIONS;
			ResolvedType[][] result = new ResolvedType[anns.length][];
			// CACHING??
			for (int i = 0; i < anns.length; i++) {
				if (anns[i] != null) {
					result[i] = new ResolvedType[anns[i].length];
					for (int j = 0; j < anns[i].length; j++) {
						result[i][j] = world.resolve(UnresolvedType.forSignature(anns[i][j].getTypeSignature()));
					}
				}
			}
			return result;
		} catch (ClassNotFoundException cnfEx) {
			// just use reflection then
		}

		// reflection...
		AccessibleObject ao = (AccessibleObject) onMember;
		Annotation[][] anns = null;
		if (onMember instanceof Method) {
			anns = ((Method) ao).getParameterAnnotations();
		} else if (onMember instanceof Constructor) {
			anns = ((Constructor<?>) ao).getParameterAnnotations();
		} else if (onMember instanceof Field) {
			// anns = null;
		}
		if (anns == null)
			return NO_PARAMETER_ANNOTATIONS;
		ResolvedType[][] result = new ResolvedType[anns.length][];
		// CACHING??
		for (int i = 0; i < anns.length; i++) {
			if (anns[i] != null) {
				result[i] = new ResolvedType[anns[i].length];
				for (int j = 0; j < anns[i].length; j++) {
					result[i][j] = UnresolvedType.forName(anns[i][j].annotationType().getName()).resolve(world);
				}
			}
		}
		return result;
	}

}
