/*
 * Copyright 2018 TNG Technology Consulting GmbH
 *
 * 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 com.tngtech.archunit.core.importer;

import java.lang.reflect.Array;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Booleans;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Chars;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.primitives.Shorts;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.base.Function;
import com.tngtech.archunit.base.Optional;
import com.tngtech.archunit.core.MayResolveTypesViaReflection;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaEnumConstant;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.core.domain.JavaType;
import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createSource;
import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME;
import static com.tngtech.archunit.core.domain.JavaStaticInitializer.STATIC_INITIALIZER_NAME;
import static com.tngtech.archunit.core.importer.ClassFileProcessor.ASM_API_VERSION;

class JavaClassProcessor extends ClassVisitor {
    private static final Logger LOG = LoggerFactory.getLogger(JavaClassProcessor.class);

    private static final AccessHandler NO_OP = new AccessHandler.NoOp();

    private DomainBuilders.JavaClassBuilder javaClassBuilder;
    private final Set<DomainBuilders.JavaAnnotationBuilder> annotations = new HashSet<>();
    private final URI sourceURI;
    private final DeclarationHandler declarationHandler;
    private final AccessHandler accessHandler;
    private String className;

    JavaClassProcessor(URI sourceURI, DeclarationHandler declarationHandler) {
        this(sourceURI, declarationHandler, NO_OP);
    }

    JavaClassProcessor(URI sourceURI, DeclarationHandler declarationHandler, AccessHandler accessHandler) {
        super(ASM_API_VERSION);
        this.sourceURI = sourceURI;
        this.declarationHandler = declarationHandler;
        this.accessHandler = accessHandler;
    }

    Optional<JavaClass> createJavaClass() {
        return javaClassBuilder != null ? Optional.of(javaClassBuilder.build()) : Optional.<JavaClass>absent();
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        LOG.info("Analysing class '{}'", name);
        JavaType javaType = JavaTypeImporter.createFromAsmObjectTypeName(name);
        if (alreadyImported(javaType)) {
            return;
        }

        ImmutableSet<String> interfaceNames = createInterfaceNames(interfaces);
        LOG.debug("Found interfaces {} on class '{}'", interfaceNames, name);
        boolean opCodeForInterfaceIsPresent = (access & Opcodes.ACC_INTERFACE) != 0;
        boolean opCodeForEnumIsPresent = (access & Opcodes.ACC_ENUM) != 0;
        Optional<String> superClassName = getSuperClassName(superName, opCodeForInterfaceIsPresent);
        LOG.debug("Found superclass {} on class '{}'", superClassName, name);

        javaClassBuilder = new DomainBuilders.JavaClassBuilder()
                .withSource(createSource(sourceURI))
                .withType(javaType)
                .withInterface(opCodeForInterfaceIsPresent)
                .withEnum(opCodeForEnumIsPresent)
                .withModifiers(JavaModifier.getModifiersForClass(access));

        className = javaType.getName();
        declarationHandler.onNewClass(className, superClassName, interfaceNames);
    }

    private boolean alreadyImported(JavaType javaType) {
        return !declarationHandler.isNew(javaType.getName());
    }

    // NOTE: For some reason ASM claims superName == java/lang/Object for Interfaces???
    //       This is inconsistent with the behavior of Class.getSuperClass()
    private Optional<String> getSuperClassName(String superName, boolean isInterface) {
        return superName != null && !isInterface ?
                Optional.of(createTypeName(superName)) :
                Optional.<String>absent();
    }

    private boolean importAborted() {
        return javaClassBuilder == null;
    }

    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        if (importAborted()) {
            return;
        }

        if (name != null && outerName != null) {
            String innerTypeName = createTypeName(name);
            correctModifiersForNestedClass(innerTypeName, access);
            declarationHandler.registerEnclosingClass(innerTypeName, createTypeName(outerName));
        }
    }

    // Modifier handling is somewhat counter intuitive for nested classes, even though we 'visit' the nested class
    // like any outer class in visit(..) before, the modifiers like 'PUBLIC' or 'PRIVATE'
    // are found in the access flags of visitInnerClass(..)
    private void correctModifiersForNestedClass(String innerTypeName, int access) {
        if (innerTypeName.equals(className)) {
            javaClassBuilder.withModifiers(JavaModifier.getModifiersForClass(access));
        }
    }

    @Override
    public void visitOuterClass(String owner, String name, String desc) {
        if (importAborted()) {
            return;
        }

        declarationHandler.registerEnclosingClass(className, createTypeName(owner));
    }

    private ImmutableSet<String> createInterfaceNames(String[] interfaces) {
        ImmutableSet.Builder<String> result = ImmutableSet.builder();
        for (String i : interfaces) {
            result.add(createTypeName(i));
        }
        return result.build();
    }

    private String createTypeName(String name) {
        return name.replace("/", ".");
    }

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

        DomainBuilders.JavaFieldBuilder fieldBuilder = new DomainBuilders.JavaFieldBuilder()
                .withName(name)
                .withType(JavaTypeImporter.importAsmType(Type.getType(desc)))
                .withModifiers(JavaModifier.getModifiersForField(access))
                .withDescriptor(desc);
        declarationHandler.onDeclaredField(fieldBuilder);
        return new FieldProcessor(fieldBuilder);
    }

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

        LOG.debug("Analysing method {}.{}:{}", className, name, desc);
        accessHandler.setContext(new CodeUnit(name, namesOf(Type.getArgumentTypes(desc)), className));

        DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder = addCodeUnitBuilder(name);
        Type methodType = Type.getMethodType(desc);
        codeUnitBuilder
                .withName(name)
                .withModifiers(JavaModifier.getModifiersForMethod(access))
                .withParameters(typesFrom(methodType.getArgumentTypes()))
                .withReturnType(JavaTypeImporter.importAsmType(methodType.getReturnType()))
                .withDescriptor(desc);

        return new MethodProcessor(className, accessHandler, codeUnitBuilder);
    }

    private List<JavaType> typesFrom(Type[] asmTypes) {
        List<JavaType> result = new ArrayList<>();
        for (Type asmType : asmTypes) {
            result.add(JavaTypeImporter.importAsmType(asmType));
        }
        return result;
    }

    private DomainBuilders.JavaCodeUnitBuilder<?, ?> addCodeUnitBuilder(String name) {
        if (CONSTRUCTOR_NAME.equals(name)) {
            DomainBuilders.JavaConstructorBuilder builder = new DomainBuilders.JavaConstructorBuilder();
            declarationHandler.onDeclaredConstructor(builder);
            return builder;
        } else if (STATIC_INITIALIZER_NAME.equals(name)) {
            DomainBuilders.JavaStaticInitializerBuilder builder = new DomainBuilders.JavaStaticInitializerBuilder();
            declarationHandler.onDeclaredStaticInitializer(builder);
            return builder;
        } else {
            DomainBuilders.JavaMethodBuilder builder = new DomainBuilders.JavaMethodBuilder();
            declarationHandler.onDeclaredMethod(builder);
            return builder;
        }
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if (importAborted()) {
            return super.visitAnnotation(desc, visible);
        }

        return new AnnotationProcessor(addAnnotationTo(annotations), annotationBuilderFor(desc));
    }

    @Override
    public void visitEnd() {
        if (importAborted()) {
            return;
        }

        declarationHandler.onDeclaredAnnotations(annotations);
        LOG.debug("Done analysing {}", className);
    }

    private static List<String> namesOf(Type[] types) {
        ImmutableList.Builder<String> result = ImmutableList.builder();
        for (Type type : types) {
            result.add(JavaTypeImporter.importAsmType(type).getName());
        }
        return result.build();
    }

    private static class MethodProcessor extends MethodVisitor {
        private final String declaringClassName;
        private final AccessHandler accessHandler;
        private final DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder;
        private final Set<DomainBuilders.JavaAnnotationBuilder> annotations = new HashSet<>();
        private int actualLineNumber;

        MethodProcessor(String declaringClassName, AccessHandler accessHandler, DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder) {
            super(ASM_API_VERSION);
            this.declaringClassName = declaringClassName;
            this.accessHandler = accessHandler;
            this.codeUnitBuilder = codeUnitBuilder;
        }

        @Override
        public void visitCode() {
            actualLineNumber = 0;
        }

        // NOTE: ASM doesn't reliably visit this method, so if this method is skipped, line number 0 is recorded
        @Override
        public void visitLineNumber(int line, Label start) {
            LOG.debug("Examining line number {}", line);
            actualLineNumber = line;
            accessHandler.setLineNumber(actualLineNumber);
        }

        @Override
        public void visitFieldInsn(int opcode, String owner, String name, String desc) {
            accessHandler.handleFieldInstruction(opcode, owner, name, desc);
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
            accessHandler.handleMethodInstruction(owner, name, desc);
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            return new AnnotationProcessor(addAnnotationTo(annotations), annotationBuilderFor(desc));
        }

        @Override
        public AnnotationVisitor visitAnnotationDefault() {
            return new AnnotationDefaultProcessor(declaringClassName, codeUnitBuilder);
        }

        @Override
        public void visitEnd() {
            codeUnitBuilder.withAnnotations(annotations);
        }

        private static class AnnotationDefaultProcessor extends AnnotationVisitor {
            private final String annotationTypeName;
            private final DomainBuilders.JavaMethodBuilder methodBuilder;

            AnnotationDefaultProcessor(String annotationTypeName, DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder) {
                super(ClassFileProcessor.ASM_API_VERSION);
                this.annotationTypeName = annotationTypeName;
                checkArgument(codeUnitBuilder instanceof DomainBuilders.JavaMethodBuilder,
                        "tried to import annotation defaults for code unit '%s' that is not a method " +
                                "(as any annotation.property() is assumed to be), " +
                                "this is likely a bug", codeUnitBuilder.getName());

                methodBuilder = (DomainBuilders.JavaMethodBuilder) codeUnitBuilder;
            }

            @Override
            public void visit(String name, Object value) {
                methodBuilder.withAnnotationDefaultValue(AnnotationTypeConversion.convert(value));
            }

            @Override
            public void visitEnum(String name, String desc, String value) {
                methodBuilder.withAnnotationDefaultValue(javaEnumBuilder(desc, value));
            }

            @Override
            public AnnotationVisitor visitAnnotation(String name, String desc) {
                return new AnnotationProcessor(new SetAsAnnotationDefault(annotationTypeName, methodBuilder), annotationBuilderFor(desc));
            }

            @Override
            public AnnotationVisitor visitArray(String name) {
                return new AnnotationArrayProcessor(new SetAsAnnotationDefault(annotationTypeName, methodBuilder));
            }

        }
    }

    private static class SetAsAnnotationDefault implements TakesAnnotationBuilder, AnnotationArrayContext {
        private final String annotationTypeName;
        private final DomainBuilders.JavaMethodBuilder methodBuilder;

        private SetAsAnnotationDefault(String annotationTypeName, DomainBuilders.JavaMethodBuilder methodBuilder) {
            this.annotationTypeName = annotationTypeName;
            this.methodBuilder = methodBuilder;
        }

        @Override
        public void add(DomainBuilders.JavaAnnotationBuilder annotation) {
            setArrayResult(DomainBuilders.JavaAnnotationBuilder.ValueBuilder.from(annotation));
        }

        @Override
        public String getDeclaringAnnotationTypeName() {
            return annotationTypeName;
        }

        @Override
        public String getDeclaringAnnotationMemberName() {
            return methodBuilder.getName();
        }

        @Override
        public void setArrayResult(DomainBuilders.JavaAnnotationBuilder.ValueBuilder valueBuilder) {
            methodBuilder.withAnnotationDefaultValue(valueBuilder);
        }
    }

    interface DeclarationHandler {
        boolean isNew(String className);

        void onNewClass(String className, Optional<String> superClassName, Set<String> interfaceNames);

        void onDeclaredField(DomainBuilders.JavaFieldBuilder fieldBuilder);

        void onDeclaredConstructor(DomainBuilders.JavaConstructorBuilder builder);

        void onDeclaredMethod(DomainBuilders.JavaMethodBuilder builder);

        void onDeclaredStaticInitializer(DomainBuilders.JavaStaticInitializerBuilder builder);

        void onDeclaredAnnotations(Set<DomainBuilders.JavaAnnotationBuilder> annotations);

        void registerEnclosingClass(String ownerName, String enclosingClassName);
    }

    interface AccessHandler {
        void handleFieldInstruction(int opcode, String owner, String name, String desc);

        void setContext(CodeUnit codeUnit);

        void setLineNumber(int lineNumber);

        void handleMethodInstruction(String owner, String name, String desc);

        @Internal
        class NoOp implements AccessHandler {
            @Override
            public void handleFieldInstruction(int opcode, String owner, String name, String desc) {
            }

            @Override
            public void setContext(CodeUnit codeUnit) {
            }

            @Override
            public void setLineNumber(int lineNumber) {
            }

            @Override
            public void handleMethodInstruction(String owner, String name, String desc) {
            }
        }
    }

    private static class FieldProcessor extends FieldVisitor {
        private final DomainBuilders.JavaFieldBuilder fieldBuilder;
        private final Set<DomainBuilders.JavaAnnotationBuilder> annotations = new HashSet<>();

        private FieldProcessor(DomainBuilders.JavaFieldBuilder fieldBuilder) {
            super(ASM_API_VERSION);

            this.fieldBuilder = fieldBuilder;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            return new AnnotationProcessor(addAnnotationTo(annotations), annotationBuilderFor(desc));
        }

        @Override
        public void visitEnd() {
            fieldBuilder.withAnnotations(annotations);
        }
    }

    private static DomainBuilders.JavaAnnotationBuilder annotationBuilderFor(String desc) {
        return new DomainBuilders.JavaAnnotationBuilder().withType(JavaTypeImporter.importAsmType(Type.getType(desc)));
    }

    private static class AnnotationProcessor extends AnnotationVisitor {
        private final TakesAnnotationBuilder takesAnnotationBuilder;
        private final DomainBuilders.JavaAnnotationBuilder annotationBuilder;

        private AnnotationProcessor(TakesAnnotationBuilder takesAnnotationBuilder, DomainBuilders.JavaAnnotationBuilder annotationBuilder) {
            super(ASM_API_VERSION);
            this.takesAnnotationBuilder = takesAnnotationBuilder;
            this.annotationBuilder = annotationBuilder;
        }

        @Override
        public void visit(String name, Object value) {
            annotationBuilder.addProperty(name, AnnotationTypeConversion.convert(value));
        }

        @Override
        public void visitEnum(String name, String desc, String value) {
            annotationBuilder.addProperty(name, javaEnumBuilder(desc, value));
        }

        @Override
        public AnnotationVisitor visitAnnotation(final String name, String desc) {
            return new AnnotationProcessor(addAnnotationAsProperty(name, annotationBuilder), annotationBuilderFor(desc));
        }

        @Override
        public AnnotationVisitor visitArray(final String name) {
            return new AnnotationArrayProcessor(new AnnotationArrayContext() {
                @Override
                public String getDeclaringAnnotationTypeName() {
                    return annotationBuilder.getJavaType().getName();
                }

                @Override
                public String getDeclaringAnnotationMemberName() {
                    return name;
                }

                @Override
                public void setArrayResult(DomainBuilders.JavaAnnotationBuilder.ValueBuilder valueBuilder) {
                    annotationBuilder.addProperty(name, valueBuilder);
                }
            });
        }

        @Override
        public void visitEnd() {
            takesAnnotationBuilder.add(annotationBuilder);
        }
    }

    private static TakesAnnotationBuilder addAnnotationTo(final Collection<? super DomainBuilders.JavaAnnotationBuilder> collection) {
        return new TakesAnnotationBuilder() {
            @Override
            public void add(DomainBuilders.JavaAnnotationBuilder annotation) {
                collection.add(annotation);
            }
        };
    }

    private static TakesAnnotationBuilder addAnnotationAsProperty(final String name, final DomainBuilders.JavaAnnotationBuilder annotationBuilder) {
        return new TakesAnnotationBuilder() {
            @Override
            public void add(DomainBuilders.JavaAnnotationBuilder builder) {
                annotationBuilder.addProperty(name, DomainBuilders.JavaAnnotationBuilder.ValueBuilder.from(builder));
            }
        };
    }

    private interface TakesAnnotationBuilder {
        void add(DomainBuilders.JavaAnnotationBuilder annotation);
    }

    private static class AnnotationArrayProcessor extends AnnotationVisitor {
        private final AnnotationArrayContext annotationArrayContext;
        private Class<?> derivedComponentType;
        private final List<DomainBuilders.JavaAnnotationBuilder.ValueBuilder> values = new ArrayList<>();

        private AnnotationArrayProcessor(AnnotationArrayContext annotationArrayContext) {
            super(ASM_API_VERSION);
            this.annotationArrayContext = annotationArrayContext;
        }

        @Override
        public void visit(String name, Object value) {
            setDerivedComponentType(value);
            values.add(AnnotationTypeConversion.convert(value));
        }

        @Override
        public AnnotationVisitor visitAnnotation(String name, String desc) {
            setDerivedComponentType(JavaAnnotation.class);
            return new AnnotationProcessor(new TakesAnnotationBuilder() {
                @Override
                public void add(DomainBuilders.JavaAnnotationBuilder annotationBuilder) {
                    values.add(DomainBuilders.JavaAnnotationBuilder.ValueBuilder.from(annotationBuilder));
                }
            }, annotationBuilderFor(desc));
        }

        @Override
        public void visitEnum(String name, final String desc, final String value) {
            setDerivedComponentType(JavaEnumConstant.class);
            values.add(javaEnumBuilder(desc, value));
        }

        private void setDerivedComponentType(Object value) {
            setDerivedComponentType(value.getClass());
        }

        // NOTE: If the declared annotation is not imported itself, we can still use this heuristic,
        //       to determine additional information about the respective array.
        //       (It the annotation is imported itself, we could easily determine this from the respective
        //       JavaClass methods)
        private void setDerivedComponentType(Class<?> type) {
            type = AnnotationTypeConversion.convert(type);
            checkState(derivedComponentType == null || derivedComponentType.equals(type),
                    "Found mixed component types while importing array, this is most likely a bug");

            derivedComponentType = type;
        }

        @Override
        public void visitEnd() {
            annotationArrayContext.setArrayResult(new ArrayValueBuilder());
        }

        private class ArrayValueBuilder extends DomainBuilders.JavaAnnotationBuilder.ValueBuilder {
            @Override
            public Optional<Object> build(ClassesByTypeName importedClasses) {
                Optional<Class<?>> componentType = determineComponentType(importedClasses);
                if (!componentType.isPresent()) {
                    return Optional.absent();
                }

                return Optional.of(toArray(componentType.get(), buildValues(importedClasses)));
            }

            @SuppressWarnings({"unchecked", "rawtypes"}) // NOTE: We assume the component type matches the list
            private Object toArray(Class<?> componentType, List<Object> values) {
                if (componentType == boolean.class) {
                    return Booleans.toArray((Collection) values);
                } else if (componentType == byte.class) {
                    return Bytes.toArray((Collection) values);
                } else if (componentType == short.class) {
                    return Shorts.toArray((Collection) values);
                } else if (componentType == int.class) {
                    return Ints.toArray((Collection) values);
                } else if (componentType == long.class) {
                    return Longs.toArray((Collection) values);
                } else if (componentType == float.class) {
                    return Floats.toArray((Collection) values);
                } else if (componentType == double.class) {
                    return Doubles.toArray((Collection) values);
                } else if (componentType == char.class) {
                    return Chars.toArray((Collection) values);
                }
                return values.toArray((Object[]) Array.newInstance(componentType, values.size()));
            }

            private List<Object> buildValues(ClassesByTypeName importedClasses) {
                List<Object> result = new ArrayList<>();
                for (DomainBuilders.JavaAnnotationBuilder.ValueBuilder value : values) {
                    result.addAll(value.build(importedClasses).asSet());
                }
                return result;
            }

            private Optional<Class<?>> determineComponentType(ClassesByTypeName importedClasses) {
                if (derivedComponentType != null) {
                    return Optional.<Class<?>>of(derivedComponentType);
                }

                JavaClass annotationType = importedClasses.get(annotationArrayContext.getDeclaringAnnotationTypeName());
                Optional<JavaMethod> method = annotationType
                        .tryGetMethod(annotationArrayContext.getDeclaringAnnotationMemberName());

                return method.isPresent() ?
                        determineComponentTypeFromReturnValue(method) :
                        Optional.<Class<?>>absent();
            }

            private Optional<Class<?>> determineComponentTypeFromReturnValue(Optional<JavaMethod> method) {
                String name = method.get().getReturnType().getName();
                Optional<Class<?>> result = AnnotationTypeConversion.tryConvert(name);
                if (result.isPresent()) {
                    return Optional.<Class<?>>of(result.get().getComponentType());
                }
                return resolveComponentTypeFrom(name);
            }

            @MayResolveTypesViaReflection(reason = "Resolving primitives does not really use reflection")
            private Optional<Class<?>> resolveComponentTypeFrom(String name) {
                JavaType type = JavaType.From.name(name);
                JavaType componentType = getComponentType(type);

                if (componentType.isPrimitive()) {
                    return Optional.<Class<?>>of(componentType.resolveClass());
                }
                if (String.class.getName().equals(componentType.getName())) {
                    return Optional.<Class<?>>of(String.class);
                }

                // if we couldn't determine the type up to now, it must be an empty enum or annotation array,
                // it's not completely consistent, but we'll just treat this as Object array for now and see
                // if this will ever make a problem, since annotation proxy would to the conversion backwards
                // and if one has to handle get(property): Object, this to be an empty Object[]
                // instead of a JavaEnumConstant[] or JavaAnnotation[] should hardly cause any real problems
                return Optional.<Class<?>>of(Object.class);
            }

            private JavaType getComponentType(JavaType type) {
                Optional<JavaType> result = type.tryGetComponentType();
                checkState(result.isPresent(), "Couldn't determine component type of array return type %s, " +
                        "this is most likely a bug", type.getName());

                return result.get();
            }
        }
    }

    private interface AnnotationArrayContext {
        String getDeclaringAnnotationTypeName();

        String getDeclaringAnnotationMemberName();

        void setArrayResult(DomainBuilders.JavaAnnotationBuilder.ValueBuilder valueBuilder);
    }

    private static DomainBuilders.JavaAnnotationBuilder.ValueBuilder javaEnumBuilder(final String desc, final String value) {
        return new DomainBuilders.JavaAnnotationBuilder.ValueBuilder() {
            @Override
            public Optional<Object> build(ClassesByTypeName importedClasses) {
                return Optional.<Object>of(
                        new DomainBuilders.JavaEnumConstantBuilder()
                                .withDeclaringClass(importedClasses.get(Type.getType(desc).getClassName()))
                                .withName(value)
                                .build());
            }
        };
    }

    private static class AnnotationTypeConversion {
        private static final Map<String, Class<?>> externalTypeToInternalType = ImmutableMap.of(
                Type.class.getName(), JavaClass.class,
                Class.class.getName(), JavaClass.class,
                Class[].class.getName(), JavaClass[].class
        );
        private static final Map<Class<?>, Function<Object, DomainBuilders.JavaAnnotationBuilder.ValueBuilder>> importedValueToInternalValue =
                ImmutableMap.<Class<?>, Function<Object, DomainBuilders.JavaAnnotationBuilder.ValueBuilder>>of(
                        Type.class, new Function<Object, DomainBuilders.JavaAnnotationBuilder.ValueBuilder>() {
                            @Override
                            public DomainBuilders.JavaAnnotationBuilder.ValueBuilder apply(final Object input) {
                                return new DomainBuilders.JavaAnnotationBuilder.ValueBuilder() {
                                    @Override
                                    public Optional<Object> build(ClassesByTypeName importedClasses) {
                                        return Optional.<Object>of(importedClasses.get(((Type) input).getClassName()));
                                    }
                                };
                            }
                        }
                );

        static Class<?> convert(Class<?> type) {
            return externalTypeToInternalType.containsKey(type.getName()) ?
                    externalTypeToInternalType.get(type.getName()) :
                    type;
        }

        static Optional<Class<?>> tryConvert(String typeName) {
            return externalTypeToInternalType.containsKey(typeName) ?
                    Optional.<Class<?>>of(externalTypeToInternalType.get(typeName)) :
                    Optional.<Class<?>>absent();
        }

        static DomainBuilders.JavaAnnotationBuilder.ValueBuilder convert(Object value) {
            return importedValueToInternalValue.containsKey(value.getClass()) ?
                    importedValueToInternalValue.get(value.getClass()).apply(value) :
                    DomainBuilders.JavaAnnotationBuilder.ValueBuilder.ofFinished(value);
        }
    }
}