/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.xbean.finder;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.xbean.asm9.original.commons.EmptyVisitor;
import org.apache.xbean.finder.util.SingleLinkedList;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

/**
 * @version $Rev: 1881759 $ $Date: 2020-09-16 10:29:43 +0200 (Wed, 16 Sep 2020) $
 */
public abstract class AbstractFinder implements IAnnotationFinder {
    private final Map<String, List<Info>> annotated = new HashMap<String, List<Info>>();
    protected final Map<String, ClassInfo> classInfos = new HashMap<String, ClassInfo>();
    protected final Map<String, ClassInfo> originalInfos = new HashMap<String, ClassInfo>();
    private final List<String> classesNotLoaded = new ArrayList<String>();
    private final int ASM_FLAGS = ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES;

    protected abstract URL getResource(String className);

    protected abstract Class<?> loadClass(String fixedName) throws ClassNotFoundException;

    public List<String> getAnnotatedClassNames() {
        return new ArrayList<String>(originalInfos.keySet());
    }

    /**
     * The link() method must be called to successfully use the findSubclasses and findImplementations methods
     * @return
     * @throws IOException
     */
    public AbstractFinder link() throws IOException {
        // already linked?
        if (originalInfos.size() > 0) return this;

        // keep track of what was originally from the archives
        originalInfos.putAll(classInfos);

        for (ClassInfo classInfo : classInfos.values().toArray(new ClassInfo[classInfos.size()])) {

            linkParent(classInfo);
        }

        for (ClassInfo classInfo : classInfos.values().toArray(new ClassInfo[classInfos.size()])) {

            linkInterfaces(classInfo);
        }

        return this;
    }

    private void linkParent(ClassInfo classInfo) throws IOException {
        if (classInfo.superType == null) return;
        if (classInfo.superType.equals("java.lang.Object")) return;
        
        ClassInfo parentInfo = classInfo.superclassInfo;

        if (parentInfo == null) {

            parentInfo = classInfos.get(classInfo.superType);

            if (parentInfo == null) {

                if (classInfo.clazz != null) {
                    readClassDef(((Class<?>) classInfo.clazz).getSuperclass());
                } else {
                    readClassDef(classInfo.superType);
                }

                parentInfo = classInfos.get(classInfo.superType);

                if (parentInfo == null) return;
                
                linkParent(parentInfo);
            }

            classInfo.superclassInfo = parentInfo;
        }

        if (!parentInfo.subclassInfos.contains(classInfo)) {
            parentInfo.subclassInfos.add(classInfo);
        }
    }

    private void linkInterfaces(ClassInfo classInfo) throws IOException {
        final List<ClassInfo> infos = new ArrayList<ClassInfo>();

        if (classInfo.clazz != null){
            final Class<?>[] interfaces = classInfo.clazz.getInterfaces();

            for (Class<?> clazz : interfaces) {
                ClassInfo interfaceInfo = classInfos.get(clazz.getName());

                if (interfaceInfo == null){
                    readClassDef(clazz);
                }

                interfaceInfo = classInfos.get(clazz.getName());

                if (interfaceInfo != null) {
                    infos.add(interfaceInfo);
                }
            }
        } else {
            for (String className : classInfo.interfaces) {
                ClassInfo interfaceInfo = classInfos.get(className);

                if (interfaceInfo == null){
                    readClassDef(className);
                }

                interfaceInfo = classInfos.get(className);

                if (interfaceInfo != null) {
                    infos.add(interfaceInfo);
                }
            }
        }

        for (ClassInfo info : infos) {
            linkInterfaces(info);
        }
    }

    public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
        List<Info> infos = annotated.get(annotation.getName());
        return infos != null && !infos.isEmpty();
    }

    /**
     * Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
     * <p/>
     * The list will only contain entries of classes whose byte code matched the requirements
     * of last invoked find* method, but were unable to be loaded and included in the results.
     * <p/>
     * The list returned is unmodifiable.  Once obtained, the returned list will be a live view of the
     * results from the last findAnnotated* method call.
     * <p/>
     * This method is not thread safe.
     * @return an unmodifiable live view of classes that could not be loaded in previous findAnnotated* call.
     */
    public List<String> getClassesNotLoaded() {
        return Collections.unmodifiableList(classesNotLoaded);
    }

    public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) {
        classesNotLoaded.clear();
        List<Package> packages = new ArrayList<Package>();
        List<Info> infos = getAnnotationInfos(annotation.getName());
        for (Info info : infos) {
            if (info instanceof PackageInfo) {
                PackageInfo packageInfo = (PackageInfo) info;
                try {
                    Package pkg = packageInfo.get();
                    // double check via proper reflection
                    if (pkg.isAnnotationPresent(annotation)) {
                        packages.add(pkg);
                    }
                } catch (ClassNotFoundException e) {
                    classesNotLoaded.add(packageInfo.getName());
                }
            }
        }
        return packages;
    }

    public List<Class<?>> findAnnotatedClasses(Class<? extends Annotation> annotation) {
        classesNotLoaded.clear();
        List<Class<?>> classes = new ArrayList<Class<?>>();
        List<Info> infos = getAnnotationInfos(annotation.getName());
        for (Info info : infos) {
            if (info instanceof ClassInfo) {
                ClassInfo classInfo = (ClassInfo) info;
                try {
                    Class clazz = classInfo.get();
                    // double check via proper reflection
                    if (clazz.isAnnotationPresent(annotation)) {
                        classes.add(clazz);
                    }
                } catch (ClassNotFoundException e) {
                    classesNotLoaded.add(classInfo.getName());
                }
            }
        }
        return classes;
    }

    public List<Annotated<Class<?>>> findMetaAnnotatedClasses(Class<? extends Annotation> annotation) {
        List<Class<?>> classes = findAnnotatedClasses(annotation);
        List<Annotated<Class<?>>> list = new ArrayList<Annotated<Class<?>>>();
        for (final Class<?> clazz : classes) {
            list.add(new MetaAnnotatedClass(clazz));
        }
        return list;
    }

    /**
     * Naive implementation - works extremelly slow O(n^3)
     *
     * @param annotation
     * @return list of directly or indirectly (inherited) annotated classes
     */
    public List<Class<?>> findInheritedAnnotatedClasses(Class<? extends Annotation> annotation) {
        classesNotLoaded.clear();
        List<Class<?>> classes = new ArrayList<Class<?>>();
        List<Info> infos = getAnnotationInfos(annotation.getName());
        for (Info info : infos) {
            try {
            	if(info instanceof ClassInfo){
                   classes.add(((ClassInfo) info).get());
            	}
            } catch (ClassNotFoundException cnfe) {
                // TODO: ignored, but a log message would be appropriate
            }
        }
        boolean annClassFound;
        List<ClassInfo> tempClassInfos = new ArrayList<ClassInfo>(classInfos.values());
        do {
            annClassFound = false;
            for (int pos = 0; pos < tempClassInfos.size(); pos++) {
                ClassInfo classInfo = tempClassInfos.get(pos);
                try {
                    // check whether any superclass is annotated
                    String superType = classInfo.getSuperType();
                    for (Class clazz : classes) {
                        if (superType.equals(clazz.getName())) {
                            classes.add(classInfo.get());
                            tempClassInfos.remove(pos);
                            annClassFound = true;
                            break;
                        }
                    }
                    // check whether any interface is annotated
                    List<String> interfces = classInfo.getInterfaces();
                    for (String interfce: interfces) {
                        for (Class clazz : classes) {
                            if (interfce.replaceFirst("<.*>","").equals(clazz.getName())) {
                                classes.add(classInfo.get());
                                tempClassInfos.remove(pos);
                                annClassFound = true;
                                break;
                            }
                        }
                    }
                } catch (ClassNotFoundException e) {
                    classesNotLoaded.add(classInfo.getName());
                } catch (NoClassDefFoundError e) {
                    classesNotLoaded.add(classInfo.getName());
                }
            }
        } while (annClassFound);
        return classes;
    }

    public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) {
        classesNotLoaded.clear();
        List<ClassInfo> seen = new ArrayList<ClassInfo>();
        List<Method> methods = new ArrayList<Method>();
        List<Info> infos = getAnnotationInfos(annotation.getName());
        for (Info info : infos) {
            if (info instanceof MethodInfo && !info.getName().equals("<init>")) {
                MethodInfo methodInfo = (MethodInfo) info;
                ClassInfo classInfo = methodInfo.getDeclaringClass();

                if (seen.contains(classInfo)) continue;

                seen.add(classInfo);

                try {
                    Class clazz = classInfo.get();
                    for (Method method : clazz.getDeclaredMethods()) {
                        if (method.isAnnotationPresent(annotation)) {
                            methods.add(method);
                        }
                    }
                } catch (ClassNotFoundException e) {
                    classesNotLoaded.add(classInfo.getName());
                }
            }
        }
        return methods;
    }

    public List<Annotated<Method>> findMetaAnnotatedMethods(Class<? extends Annotation> annotation) {
        List<Method> methods = findAnnotatedMethods(annotation);
        List<Annotated<Method>> list = new ArrayList<Annotated<Method>>();
        for (final Method method : methods) {
            list.add(new MetaAnnotatedMethod(method));
        }
        return list;
    }

    public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) {
        classesNotLoaded.clear();
        List<ClassInfo> seen = new ArrayList<ClassInfo>();
        List<Constructor> constructors = new ArrayList<Constructor>();
        List<Info> infos = getAnnotationInfos(annotation.getName());
        for (Info info : infos) {
            if (info instanceof MethodInfo && info.getName().equals("<init>")) {
                MethodInfo methodInfo = (MethodInfo) info;
                ClassInfo classInfo = methodInfo.getDeclaringClass();

                if (seen.contains(classInfo)) continue;

                seen.add(classInfo);

                try {
                    Class clazz = classInfo.get();
                    for (Constructor constructor : clazz.getConstructors()) {
                        if (constructor.isAnnotationPresent(annotation)) {
                            constructors.add(constructor);
                        }
                    }
                } catch (ClassNotFoundException e) {
                    classesNotLoaded.add(classInfo.getName());
                }
            }
        }
        return constructors;
    }

    public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) {
        classesNotLoaded.clear();
        List<ClassInfo> seen = new ArrayList<ClassInfo>();
        List<Field> fields = new ArrayList<Field>();
        List<Info> infos = getAnnotationInfos(annotation.getName());
        for (Info info : infos) {
            if (info instanceof FieldInfo) {
                FieldInfo fieldInfo = (FieldInfo) info;
                ClassInfo classInfo = fieldInfo.getDeclaringClass();

                if (seen.contains(classInfo)) continue;

                seen.add(classInfo);

                try {
                    Class clazz = classInfo.get();
                    for (Field field : clazz.getDeclaredFields()) {
                        if (field.isAnnotationPresent(annotation)) {
                            fields.add(field);
                        }
                    }
                } catch (ClassNotFoundException e) {
                    classesNotLoaded.add(classInfo.getName());
                }
            }
        }
        return fields;
    }

    public List<Annotated<Field>> findMetaAnnotatedFields(Class<? extends Annotation> annotation) {
        List<Field> fields = findAnnotatedFields(annotation);
        List<Annotated<Field>> list = new ArrayList<Annotated<Field>>();
        for (final Field field : fields) {
            list.add(new MetaAnnotatedField(field));
        }

        return list;
    }

    public List<Class<?>> findClassesInPackage(String packageName, boolean recursive) {
        classesNotLoaded.clear();
        List<Class<?>> classes = new ArrayList<Class<?>>();
        for (ClassInfo classInfo : classInfos.values()) {
            try {
                if (recursive && classInfo.getPackageName().startsWith(packageName)){
                    classes.add(classInfo.get());
                } else if (classInfo.getPackageName().equals(packageName)){
                    classes.add(classInfo.get());
                }
            } catch (ClassNotFoundException e) {
                classesNotLoaded.add(classInfo.getName());
            }
        }
        return classes;
    }

    public <T> List<Class<? extends T>> findSubclasses(Class<T> clazz) {
        if (clazz == null) throw new NullPointerException("class cannot be null");

        classesNotLoaded.clear();

        final ClassInfo classInfo = classInfos.get(clazz.getName());

        List<Class<? extends T>> found = new ArrayList<Class<? extends T>>();

        if (classInfo == null) return found;

        findSubclasses(classInfo, found, clazz);

        return found;
    }

    private <T> void findSubclasses(ClassInfo classInfo, List<Class<? extends T>> found, Class<T> clazz) {

        for (ClassInfo subclassInfo : classInfo.subclassInfos) {

            try {
                found.add(subclassInfo.get().asSubclass(clazz));
            } catch (ClassNotFoundException e) {
                classesNotLoaded.add(subclassInfo.getName());
            }

            findSubclasses(subclassInfo, found, clazz);
        }
    }

    private <T> List<Class<? extends T>> _findSubclasses(Class<T> clazz) {
        if (clazz == null) throw new NullPointerException("class cannot be null");

        List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>();


        for (ClassInfo classInfo : classInfos.values()) {

            try {

                if (clazz.getName().equals(classInfo.superType)) {

                    if (clazz.isAssignableFrom(classInfo.get())) {

                        classes.add(classInfo.get().asSubclass(clazz));

                        classes.addAll(_findSubclasses(classInfo.get().asSubclass(clazz)));
                    }
                }

            } catch (ClassNotFoundException e) {
                classesNotLoaded.add(classInfo.getName());
            }

        }

        return classes;
    }

    public <T> List<Class<? extends T>> findImplementations(Class<T> clazz) {
        if (clazz == null) throw new NullPointerException("class cannot be null");
        if (!clazz.isInterface()) new IllegalArgumentException("class must be an interface");
        classesNotLoaded.clear();

        final String interfaceName = clazz.getName();

        // Collect all interfaces extending the main interface (recursively)
        // Collect all implementations of interfaces
        // i.e. all *directly* implementing classes
        List<ClassInfo> infos = collectImplementations(interfaceName);

        // Collect all subclasses of implementations
        List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>();
        for (ClassInfo info : infos) {
            try {
                final Class<? extends T> impl = (Class<? extends T>) info.get();

                if (clazz.isAssignableFrom(impl)) {
                    classes.add(impl);

                    // Optimization: Don't need to call this method if parent class was already searched



                    classes.addAll(_findSubclasses(impl));
                }

            } catch (ClassNotFoundException e) {
                classesNotLoaded.add(info.getName());
            }
        }
        return classes;
    }

    private List<ClassInfo> collectImplementations(String interfaceName) {
        final List<ClassInfo> infos = new ArrayList<ClassInfo>();

        for (ClassInfo classInfo : classInfos.values()) {

            if (classInfo.interfaces.contains(interfaceName)) {

                infos.add(classInfo);

                try {

                    final Class clazz = classInfo.get();

                    if (clazz.isInterface() && !clazz.isAnnotation()) {

                        infos.addAll(collectImplementations(classInfo.name));

                    }
                    
                } catch (ClassNotFoundException ignore) {
                    // we'll deal with this later
                }
            }
        }
        return infos;
    }

    protected List<Info> getAnnotationInfos(String name) {
        List<Info> infos = annotated.get(name);
        if (infos == null) {
            infos = new SingleLinkedList<Info>();
            annotated.put(name, infos);
        }
        return infos;
    }

    protected void readClassDef(String className) {
        int pos = className.indexOf("<");
        if (pos > -1) {
            className = className.substring(0, pos);
        }
        pos = className.indexOf(">");
        if (pos > -1) {
            className = className.substring(0, pos);
        }
        if (!className.endsWith(".class")) {
            className = className.replace('.', '/') + ".class";
        }
        try {
            // TODO: check out META-INF/versions/<version>/className
            URL resource = getResource(className);
            if (resource != null) {
                InputStream in = resource.openStream();
                try {
                    readClassDef(in);
                } finally {
                    in.close();
                }
            } else {
                classesNotLoaded.add(className + " (no resource found for class)");
            }
        } catch (IOException e) {
            classesNotLoaded.add(className + e.getMessage());
        }

    }

    protected void readClassDef(InputStream in) throws IOException {
        readClassDef(in, null);
    }

    protected void readClassDef(InputStream in, String path) throws IOException {
        ClassReader classReader = new ClassReader(in);
        classReader.accept(new InfoBuildingVisitor(path), ASM_FLAGS);
    }

    protected void readClassDef(Class clazz) {
        List<Info> infos = new ArrayList<Info>();

        Package aPackage = clazz.getPackage();
        if (aPackage != null){
            final PackageInfo info = new PackageInfo(aPackage);
            for (AnnotationInfo annotation : info.getAnnotations()) {
                List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
                if (!annotationInfos.contains(info)) {
                    annotationInfos.add(info);
                }
            }
        }

        ClassInfo classInfo = new ClassInfo(clazz);
        infos.add(classInfo);
        classInfos.put(clazz.getName(), classInfo);
        for (Method method : clazz.getDeclaredMethods()) {
            infos.add(new MethodInfo(classInfo, method));
        }

        for (Constructor constructor : clazz.getConstructors()) {
            infos.add(new MethodInfo(classInfo, constructor));
        }

        for (Field field : clazz.getDeclaredFields()) {
            infos.add(new FieldInfo(classInfo, field));
        }

        for (Info info : infos) {
            for (AnnotationInfo annotation : info.getAnnotations()) {
                List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
                annotationInfos.add(info);
            }
        }
    }

    public class Annotatable {
        private final List<AnnotationInfo> annotations = new ArrayList<AnnotationInfo>();

        public Annotatable(AnnotatedElement element) {
            for (Annotation annotation : getAnnotations(element)) {
                annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
            }
        }

        public Annotatable() {
        }

        public List<AnnotationInfo> getAnnotations() {
            return annotations;
        }
        
        /**
         * Utility method to get around some errors caused by 
         * interactions between the Equinox class loaders and 
         * the OpenJPA transformation process.  There is a window 
         * where the OpenJPA transformation process can cause 
         * an annotation being processed to get defined in a 
         * classloader during the actual defineClass call for 
         * that very class (e.g., recursively).  This results in 
         * a LinkageError exception.  If we see one of these, 
         * retry the request.  Since the annotation will be 
         * defined on the second pass, this should succeed.  If 
         * we get a second exception, then it's likely some 
         * other problem. 
         * 
         * @param element The AnnotatedElement we need information for.
         * 
         * @return An array of the Annotations defined on the element. 
         */
        private Annotation[] getAnnotations(AnnotatedElement element) {
            try {
                return element.getAnnotations();
            } catch (LinkageError e) {
                return element.getAnnotations();
            }
        }

    }

    public static interface Info {
        String getName();

        List<AnnotationInfo> getAnnotations();
    }

    public class PackageInfo extends Annotatable implements Info {
        private final String name;
        private final ClassInfo info;
        private final Package pkg;

        public PackageInfo(Package pkg){
            super(pkg);
            this.pkg = pkg;
            this.name = pkg.getName();
            this.info = null;
        }

        public PackageInfo(String name) {
            info = new ClassInfo(name, null);
            this.name = name;
            this.pkg = null;
        }

        public String getName() {
            return name;
        }

        public Package get() throws ClassNotFoundException {
            return (pkg != null)?pkg:info.get().getPackage();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            PackageInfo that = (PackageInfo) o;

            if (name != null ? !name.equals(that.name) : that.name != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            return name != null ? name.hashCode() : 0;
        }
    }

    public class ClassInfo extends Annotatable implements Info {
        private String name;
        private final List<MethodInfo> methods = new SingleLinkedList<MethodInfo>();
        private final List<MethodInfo> constructors = new SingleLinkedList<MethodInfo>();
        private String superType;
        private ClassInfo superclassInfo;
        private final List<ClassInfo> subclassInfos = new SingleLinkedList<ClassInfo>();
        private final List<String> interfaces = new SingleLinkedList<String>();
        private final List<FieldInfo> fields = new SingleLinkedList<FieldInfo>();
        //e.g. bundle class path prefix.
        private String path;
        private Class<?> clazz;

        public ClassInfo(Class clazz) {
            super(clazz);
            this.clazz = clazz;
            this.name = clazz.getName();
            Class superclass = clazz.getSuperclass();
            this.superType = superclass != null ? superclass.getName(): null;
            for (Class intrface : clazz.getInterfaces()) {
                this.interfaces.add(intrface.getName());
            }
        }

        public ClassInfo(String name, String superType) {
            this.name = name;
            this.superType = superType;
        }

        public String getPackageName(){
            return name.indexOf(".") > 0 ? name.substring(0, name.lastIndexOf(".")) : "" ;
        }

        public List<MethodInfo> getConstructors() {
            return constructors;
        }

        public List<String> getInterfaces() {
            return interfaces;
        }

        public List<FieldInfo> getFields() {
            return fields;
        }

        public List<MethodInfo> getMethods() {
            return methods;
        }

        public String getName() {
            return name;
        }

        public String getSuperType() {
            return superType;
        }

        public Class<?> get() throws ClassNotFoundException {
            if (clazz != null) return clazz;
            try {
                String fixedName = name.replaceFirst("<.*>", "");
                this.clazz = loadClass(fixedName);
                return clazz;
            } catch (ClassNotFoundException notFound) {
                classesNotLoaded.add(name);
                throw notFound;
            }
        }

        public String toString() {
            return name;
        }

        public String getPath() {
            return path;
        }
    }

    public class MethodInfo extends Annotatable implements Info {
        private final ClassInfo declaringClass;
        private final String returnType;
        private final String name;
        private final List<List<AnnotationInfo>> parameterAnnotations = new ArrayList<List<AnnotationInfo>>();

        public MethodInfo(ClassInfo info, Constructor constructor){
            super(constructor);
            this.declaringClass = info;
            this.name = "<init>";
            this.returnType = Void.TYPE.getName();
        }

        public MethodInfo(ClassInfo info, Method method){
            super(method);
            this.declaringClass = info;
            this.name = method.getName();
            this.returnType = method.getReturnType().getName();
        }

        public MethodInfo(ClassInfo declarignClass, String name, String returnType) {
            this.declaringClass = declarignClass;
            this.name = name;
            this.returnType = returnType;
        }

        public List<List<AnnotationInfo>> getParameterAnnotations() {
            return parameterAnnotations;
        }

        public List<AnnotationInfo> getParameterAnnotations(int index) {
            if (index >= parameterAnnotations.size()) {
                for (int i = parameterAnnotations.size(); i <= index; i++) {
                    List<AnnotationInfo> annotationInfos = new ArrayList<AnnotationInfo>();
                    parameterAnnotations.add(i, annotationInfos);
                }
            }
            return parameterAnnotations.get(index);
        }

        public String getName() {
            return name;
        }

        public ClassInfo getDeclaringClass() {
            return declaringClass;
        }

        public String getReturnType() {
            return returnType;
        }

        public String toString() {
            return declaringClass + "@" + name;
        }
    }

    public class FieldInfo extends Annotatable implements Info {
        private final String name;
        private final String type;
        private final ClassInfo declaringClass;

        public FieldInfo(ClassInfo info, Field field){
            super(field);
            this.declaringClass = info;
            this.name = field.getName();
            this.type = field.getType().getName();
        }

        public FieldInfo(ClassInfo declaringClass, String name, String type) {
            this.declaringClass = declaringClass;
            this.name = name;
            this.type = type;
        }

        public String getName() {
            return name;
        }

        public ClassInfo getDeclaringClass() {
            return declaringClass;
        }

        public String getType() {
            return type;
        }

        public String toString() {
            return declaringClass + "#" + name;
        }
    }

    public class AnnotationInfo extends Annotatable implements Info {
        private final String name;

        public AnnotationInfo(Annotation annotation){
            this(annotation.getClass().getName());
        }

        public AnnotationInfo(Class<? extends Annotation> annotation) {
            this.name = annotation.getName().intern();
        }

        public AnnotationInfo(String name) {
            name = name.replaceAll("^L|;$", "");
            name = name.replace('/', '.');
            this.name = name.intern();
        }

        public String getName() {
            return name;
        }

        public String toString() {
            return name;
        }
    }

    public class InfoBuildingVisitor extends EmptyVisitor {
        private Info info;
        private String path;

        public InfoBuildingVisitor(String path) {
            this.path = path;
        }

        public InfoBuildingVisitor(Info info) {
            this.info = info;
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            if (name.endsWith("package-info")) {
                info = new PackageInfo(javaName(name));
            } else {
                ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName));
                classInfo.path = path;
//                if (signature == null) {
                    for (String interfce : interfaces) {
                        classInfo.getInterfaces().add(javaName(interfce));
                    }
//                } else {
//                    // the class uses generics
//                    new SignatureReader(signature).accept(new GenericAwareInfoBuildingVisitor(GenericAwareInfoBuildingVisitor.TYPE.CLASS, classInfo));
//                }
                info = classInfo;
                classInfos.put(classInfo.getName(), classInfo);
            }
        }

        private String javaName(String name) {
            return (name == null)? null:name.replace('/', '.');
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            AnnotationInfo annotationInfo = new AnnotationInfo(desc);
            info.getAnnotations().add(annotationInfo);
            getAnnotationInfos(annotationInfo.getName()).add(info);
            return new InfoBuildingVisitor(annotationInfo).annotationVisitor();
        }

        @Override
        public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
            ClassInfo classInfo = ((ClassInfo) info);
            FieldInfo fieldInfo = new FieldInfo(classInfo, name, desc);
            classInfo.getFields().add(fieldInfo);
            return new InfoBuildingVisitor(fieldInfo).fieldVisitor();
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            ClassInfo classInfo = ((ClassInfo) info);
            MethodInfo methodInfo = new MethodInfo(classInfo, name, desc);
            classInfo.getMethods().add(methodInfo);
            return new InfoBuildingVisitor(methodInfo).methodVisitor();
        }

        @Override
        public AnnotationVisitor visitMethodParameterAnnotation(int param, String desc, boolean visible) {
            MethodInfo methodInfo = ((MethodInfo) info);
            List<AnnotationInfo> annotationInfos = methodInfo.getParameterAnnotations(param);
            AnnotationInfo annotationInfo = new AnnotationInfo(desc);
            annotationInfos.add(annotationInfo);
            return new InfoBuildingVisitor(annotationInfo).annotationVisitor();
        }
    }
}
