package com.netflix.governator.lifecycle;

import static org.objectweb.asm.Type.ARRAY;
import static org.objectweb.asm.Type.BOOLEAN;
import static org.objectweb.asm.Type.BYTE;
import static org.objectweb.asm.Type.CHAR;
import static org.objectweb.asm.Type.DOUBLE;
import static org.objectweb.asm.Type.FLOAT;
import static org.objectweb.asm.Type.INT;
import static org.objectweb.asm.Type.LONG;
import static org.objectweb.asm.Type.OBJECT;
import static org.objectweb.asm.Type.SHORT;
import static org.objectweb.asm.Type.getArgumentTypes;
import static org.objectweb.asm.Type.getType;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class AnnotationFinder extends ClassVisitor {
	private static Logger log = LoggerFactory.getLogger(AnnotationFinder.class);
	
    private Set<Type> annotationTypes;

    private Set<Class<?>> annotatedClasses = Collections.emptySet();
    private Set<Method> annotatedMethods = new HashSet<>();
    private Set<Constructor> annotatedConstructors = new HashSet<>();
    private Set<Field> annotatedFields = new HashSet<>();

    private String className;
    private Class<?> clazz;
    private ClassLoader classLoader;

    private Class<?> selfClass() {
        if (clazz == null)
            clazz = classFromInternalName(className);
        return clazz;
    }

    private Class<?> classFromInternalName(String name) {
        try {
            return Class.forName(name.replace('/', '.'), false, classLoader);
        }  catch (ClassNotFoundException e) {
            throw new IllegalStateException("Unable to find class " + name, e);
        }
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    public AnnotationFinder(ClassLoader classLoader, Collection<Class<? extends Annotation>> annotations) {
        super(Opcodes.ASM5);
        annotationTypes = new HashSet<>();
        for (Class<?> annotation : annotations)
            annotationTypes.add(getType(annotation));
        this.classLoader = classLoader;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        Type type = getType(desc);
        for (Type annotationType : annotationTypes)  {
            if (annotationType.equals(type)) {
                annotatedClasses = Collections.<Class<?>>singleton(selfClass());
                break;
            }
        }

        return super.visitAnnotation(desc, visible);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return new AnnotationSeekingFieldVisitor(name, super.visitField(access, name, desc, signature, value));
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return new AnnotationSeekingMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions), name, desc);
    }

    private class AnnotationSeekingFieldVisitor extends FieldVisitor {
        String name;

        public AnnotationSeekingFieldVisitor(String name, FieldVisitor fv) {
            super(Opcodes.ASM5, fv);
            this.name = name;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            Type type = getType(desc);
            for (Type annotationType : annotationTypes) {
                if (annotationType.equals(type)) {
                    try {
                        annotatedFields.add(selfClass().getDeclaredField(name));
                        break;
                    } catch (NoSuchFieldException e) {
                        throw new IllegalStateException("Error visiting field " + name, e);
                    } catch (NoClassDefFoundError e) {
                        log.info("Unable to scan field '{}' of class '{}'", name, selfClass().getName(), e.getMessage());
                    }
                }
            }

            return super.visitAnnotation(desc, visible);
        }
    }

    private class AnnotationSeekingMethodVisitor extends MethodVisitor {
        String name;
        String methodDesc;

        public AnnotationSeekingMethodVisitor(MethodVisitor mv, String name, String desc) {
            super(Opcodes.ASM5, mv);
            this.name = name;
            this.methodDesc = desc;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            Type type = getType(desc);
            for (Type annotationType : annotationTypes) {
                if (annotationType.equals(type)) {
                    Type[] args = methodDesc == null 
                            ? new Type[0]
                            : getArgumentTypes(methodDesc);
                    Class[] argClasses = new Class[args.length];
                    for (int i = 0; i < args.length; i++) {
                        switch (args[i].getSort()) {
                        case OBJECT:
                        case ARRAY:
                            argClasses[i] = classFromInternalName(args[i].getInternalName());
                            break;
                        case BOOLEAN:
                            argClasses[i] = boolean.class;
                            break;
                        case BYTE:
                            argClasses[i] = byte.class;
                            break;
                        case CHAR:
                            argClasses[i] = char.class;
                            break;
                        case DOUBLE:
                            argClasses[i] = double.class;
                            break;
                        case FLOAT:
                            argClasses[i] = float.class;
                            break;
                        case INT:
                            argClasses[i] = int.class;
                            break;
                        case LONG:
                            argClasses[i] = long.class;
                            break;
                        case SHORT:
                            argClasses[i] = short.class;
                            break;
                        }
                    }

                    try {
                        if ("<init>".equals(name))
                            annotatedConstructors.add(selfClass()
                                    .getDeclaredConstructor(argClasses));
                        else
                            annotatedMethods.add(selfClass().getDeclaredMethod(
                                    name, argClasses));
                    } catch (NoClassDefFoundError e) {
                    	log.info("Unable to scan constructor of '{}' NoClassDefFoundError looking for '{}'", selfClass().getName(), e.getMessage());
                    } catch (NoSuchMethodException e) {
                        throw new IllegalStateException(e);
                    }

                    break;
                }
            }

            return super.visitAnnotation(desc, visible);
        }
    }

    /**
     * @return a 0 or 1 element Set, depending on whether the class being
     *         visited has a matching class annotation
     */
    public Set<Class<?>> getAnnotatedClasses() {
        return annotatedClasses;
    }

    public Set<Method> getAnnotatedMethods() {
        return annotatedMethods;
    }

    public Set<Constructor> getAnnotatedConstructors() {
        return annotatedConstructors;
    }

    public Set<Field> getAnnotatedFields() {
        return annotatedFields;
    }
}

@@ -4342,32 +4342,71 @@
 g field %22 + name
+ + %22 of class %22 + selfClass().getName()
 , e);%0A          

Include class name in exception