/*
 * 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.gradle.internal.reflect.annotations.impl;

import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import org.apache.commons.lang3.StringUtils;
import org.gradle.api.Action;
import org.gradle.api.problems.internal.GradleCoreProblemGroup;
import org.gradle.cache.Cache;
import org.gradle.cache.internal.ClassCacheFactory;
import org.gradle.internal.deprecation.DeprecationLogger;
import org.gradle.internal.reflect.PropertyAccessorType;
import org.gradle.internal.reflect.annotations.AnnotationCategory;
import org.gradle.internal.reflect.annotations.FunctionAnnotationMetadata;
import org.gradle.internal.reflect.annotations.HasAnnotationMetadata;
import org.gradle.internal.reflect.annotations.PropertyAnnotationMetadata;
import org.gradle.internal.reflect.annotations.TypeAnnotationMetadata;
import org.gradle.internal.reflect.annotations.TypeAnnotationMetadataStore;
import org.gradle.internal.reflect.validation.ReplayingTypeValidationContext;
import org.gradle.internal.reflect.validation.TypeAwareProblemBuilder;
import org.gradle.internal.reflect.validation.TypeValidationContext;
import org.gradle.util.internal.TextUtil;

import javax.inject.Inject;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import static org.gradle.internal.deprecation.Documentation.userManual;
import static org.gradle.internal.reflect.Methods.SIGNATURE_EQUIVALENCE;
import static org.gradle.internal.reflect.annotations.AnnotationCategory.TYPE;

public class DefaultTypeAnnotationMetadataStore implements TypeAnnotationMetadataStore {
    private static final TypeAnnotationMetadata EMPTY_TYPE_ANNOTATION_METADATA = new TypeAnnotationMetadata() {
        @Override
        public ImmutableSet<Annotation> getAnnotations() {
            return ImmutableSet.of();
        }

        @Override
        public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
            return false;
        }

        @Override
        public ImmutableSortedSet<PropertyAnnotationMetadata> getPropertiesAnnotationMetadata() {
            return ImmutableSortedSet.of();
        }

        @Override
        public ImmutableSortedSet<FunctionAnnotationMetadata> getFunctionAnnotationMetadata() {
            return ImmutableSortedSet.of();
        }

        @Override
        public void visitValidationFailures(TypeValidationContext validationContext) {
        }

        @Override
        public <T extends Annotation> Optional<T> getAnnotation(Class<T> annotationType) {
            return Optional.empty();
        }
    };

    private final ImmutableSet<Class<? extends Annotation>> recordedTypeAnnotations;
    private final ImmutableSet<String> ignoredPackagePrefixes;
    private final ImmutableMap<Class<? extends Annotation>, AnnotationCategory> propertyAnnotationCategories;
    private final ImmutableMap<Class<? extends Annotation>, AnnotationCategory> functionAnnotationCategories;
    private final Cache<Class<?>, TypeAnnotationMetadata> cache;
    private final ImmutableSet<String> potentiallyIgnoredMethodNames;
    private final ImmutableSet<Equivalence.Wrapper<Method>> globallyIgnoredMethods;
    private final ImmutableSet<Class<?>> mutableNonFinalClasses;
    private final ImmutableSet<Class<? extends Annotation>> ignoredMethodAnnotations;
    private final ImmutableSet<Class<? extends Annotation>> ignoredMethodAnnotationsAllowedModifiers;
    private final Predicate<? super Method> generatedMethodDetector;

    /**
     * Constructs the store.
     *
     * @param recordedTypeAnnotations Annotations on the type itself that should be gathered.
     * @param propertyAnnotationCategories Annotations on the properties that should be gathered. They are mapped to {@linkplain AnnotationCategory annotation categories}. The {@code ignoredMethodAnnotations} and the {@literal @}{@link Inject} annotations are automatically mapped to the {@link AnnotationCategory#TYPE TYPE} category.
     * @param functionAnnotationCategories Annotations on the functions that should be gathered. They are mapped to {@linkplain AnnotationCategory annotation categories}. The {@code ignoredMethodAnnotations} and the {@literal @}{@link Inject} annotations are automatically mapped to the {@link AnnotationCategory#TYPE TYPE} category.
     * @param ignoredPackagePrefixes Packages to ignore. Types from ignored packages are considered having no type annotations nor any annotated properties.
     * @param ignoredSuperTypes Super-types to ignore. Ignored super-types are considered having no type annotations nor any annotated properties.
     * @param ignoreMethodsFromTypes Methods to ignore: any methods declared by these types are ignored even when overridden by a given type. This is to avoid detecting methods like {@code Object.equals()} or {@code GroovyObject.getMetaClass()}.
     * @param ignoredMethodAnnotations Annotations to use to explicitly ignore a method/property.
     * @param ignoredMethodAnnotationsAllowedModifiers Annotations allowed to be used with the ignore annotations.
     * @param generatedMethodDetector Predicate to test if a method was generated (vs. being provided explicitly by the user).
     * @param mutableNonFinalClasses Mutable classes that shouldn't need explicit setters
     * @param cacheFactory A factory to create cross-build in-memory caches.
     */
    public DefaultTypeAnnotationMetadataStore(
        Collection<Class<? extends Annotation>> recordedTypeAnnotations,
        Map<Class<? extends Annotation>, ? extends AnnotationCategory> propertyAnnotationCategories,
        Map<Class<? extends Annotation>, ? extends AnnotationCategory> functionAnnotationCategories,
        Collection<String> ignoredPackagePrefixes,
        Collection<Class<?>> ignoredSuperTypes,
        Collection<Class<?>> ignoreMethodsFromTypes,
        Collection<Class<?>> mutableNonFinalClasses,
        Collection<Class<? extends Annotation>> ignoredMethodAnnotations,
        Collection<Class<? extends Annotation>> ignoredMethodAnnotationsAllowedModifiers,
        Predicate<? super Method> generatedMethodDetector,
        ClassCacheFactory cacheFactory
    ) {
        this.recordedTypeAnnotations = ImmutableSet.copyOf(recordedTypeAnnotations);
        this.ignoredPackagePrefixes = collectIgnoredPackagePrefixes(ignoredPackagePrefixes);
        this.propertyAnnotationCategories = allAnnotationCategoriesForProperties(propertyAnnotationCategories, ignoredMethodAnnotations);
        this.functionAnnotationCategories = allAnnotationCategories(functionAnnotationCategories, Collections.emptyList());
        this.cache = initCache(ignoredSuperTypes, cacheFactory);
        this.potentiallyIgnoredMethodNames = allMethodNamesOf(ignoreMethodsFromTypes);
        this.globallyIgnoredMethods = allMethodsOf(ignoreMethodsFromTypes);
        this.mutableNonFinalClasses = ImmutableSet.copyOf(mutableNonFinalClasses);
        this.ignoredMethodAnnotations = ImmutableSet.copyOf(ignoredMethodAnnotations);
        this.ignoredMethodAnnotationsAllowedModifiers = ImmutableSet.copyOf(ignoredMethodAnnotationsAllowedModifiers);
        this.generatedMethodDetector = generatedMethodDetector;
    }

    private static ImmutableSet<String> collectIgnoredPackagePrefixes(Collection<String> ignoredPackagePrefixes) {
        return ImmutableSet.copyOf(ignoredPackagePrefixes.stream()
            .map(prefix -> prefix + ".")
            .collect(Collectors.toList())
        );
    }

    private static ImmutableMap<Class<? extends Annotation>, AnnotationCategory> allAnnotationCategoriesForProperties(
        Map<Class<? extends Annotation>, ? extends AnnotationCategory> propertyAnnotationCategories,
        Collection<Class<? extends Annotation>> ignoredMethodAnnotations
    ) {
        ImmutableMap.Builder<Class<? extends Annotation>, AnnotationCategory> builder = ImmutableMap.builder();
        builder.putAll(allAnnotationCategories(propertyAnnotationCategories, ignoredMethodAnnotations));
        builder.put(Inject.class, TYPE);
        return builder.build();
    }

    private static ImmutableMap<Class<? extends Annotation>, AnnotationCategory> allAnnotationCategories(
        Map<Class<? extends Annotation>, ? extends AnnotationCategory> annotationCategories,
        Collection<Class<? extends Annotation>> ignoredMethodAnnotations
    ) {
        ImmutableMap.Builder<Class<? extends Annotation>, AnnotationCategory> builder = ImmutableMap.builder();
        builder.putAll(annotationCategories);
        for (Class<? extends Annotation> ignoredMethodAnnotation : ignoredMethodAnnotations) {
            builder.put(ignoredMethodAnnotation, TYPE);
        }
        return builder.build();
    }

    private static Cache<Class<?>, TypeAnnotationMetadata> initCache(Collection<Class<?>> ignoredSuperTypes, ClassCacheFactory cacheFactory) {
        Cache<Class<?>, TypeAnnotationMetadata> result = cacheFactory.newClassCache();
        for (Class<?> ignoredSuperType : ignoredSuperTypes) {
            result.put(ignoredSuperType, EMPTY_TYPE_ANNOTATION_METADATA);
        }
        return result;
    }

    private static ImmutableSet<String> allMethodNamesOf(Iterable<Class<?>> classes) {
        ImmutableSet.Builder<String> methods = ImmutableSet.builder();
        for (Class<?> clazz : classes) {
            for (Method method : clazz.getMethods()) {
                methods.add(method.getName());
            }
        }
        return methods.build();
    }

    private static ImmutableSet<Equivalence.Wrapper<Method>> allMethodsOf(Iterable<Class<?>> classes) {
        ImmutableSet.Builder<Equivalence.Wrapper<Method>> methods = ImmutableSet.builder();
        for (Class<?> clazz : classes) {
            for (Method method : clazz.getMethods()) {
                methods.add(SIGNATURE_EQUIVALENCE.wrap(method));
            }
        }
        return methods.build();
    }

    @Override
    public TypeAnnotationMetadata getTypeAnnotationMetadata(Class<?> type) {
        return cache.get(type, this::createTypeAnnotationMetadata);
    }

    private TypeAnnotationMetadata createTypeAnnotationMetadata(Class<?> type) {
        if (type.isPrimitive() || type.isArray() || type.isAnnotation()) {
            return EMPTY_TYPE_ANNOTATION_METADATA;
        }

        Package typePackage = type.getPackage();
        if (typePackage != null) {
            String typePackageName = typePackage.getName();
            if (ignoredPackagePrefixes.stream().anyMatch(typePackageName::startsWith)) {
                return EMPTY_TYPE_ANNOTATION_METADATA;
            }
        }

        ImmutableSet.Builder<Annotation> typeAnnotations = ImmutableSet.builder();
        for (Annotation typeAnnotation : type.getDeclaredAnnotations()) {
            if (recordedTypeAnnotations.contains(typeAnnotation.annotationType())) {
                typeAnnotations.add(typeAnnotation);
            }
        }

        Map<String, PropertyAnnotationMetadataBuilder> propertyMethodBuilders = new HashMap<>();
        Map<MethodSignature, FunctionAnnotationMetadataBuilder> functionMethodBuilders = new HashMap<>();
        ReplayingTypeValidationContext validationContext = new ReplayingTypeValidationContext();

        inheritPropertyMethods(type, validationContext, propertyMethodBuilders);
        inheritFunctionMethods(type, validationContext, functionMethodBuilders);

        ImmutableSortedSet<PropertyAnnotationMetadata> propertiesMetadata;
        ImmutableSortedSet<FunctionAnnotationMetadata> functionMetadata;
        if (!type.isSynthetic()) {
            propertiesMetadata = extractPropertiesFrom(type, propertyMethodBuilders, validationContext);
            functionMetadata = extractFunctionsFrom(type, functionMethodBuilders, validationContext);
        } else {
            ImmutableSortedSet.Builder<PropertyAnnotationMetadata> propertiesMetadataBuilder = ImmutableSortedSet.naturalOrder();
            for (PropertyAnnotationMetadataBuilder propertyMetadataBuilder : propertyMethodBuilders.values()) {
                propertiesMetadataBuilder.add(propertyMetadataBuilder.build());
            }
            propertiesMetadata = propertiesMetadataBuilder.build();

            ImmutableSortedSet.Builder<FunctionAnnotationMetadata> functionsMetadataBuilder = ImmutableSortedSet.naturalOrder();
            for (FunctionAnnotationMetadataBuilder functionMetadataBuilder : functionMethodBuilders.values()) {
                functionsMetadataBuilder.add(functionMetadataBuilder.build());
            }
            functionMetadata = functionsMetadataBuilder.build();
        }

        return new DefaultTypeAnnotationMetadata(typeAnnotations.build(), propertiesMetadata, functionMetadata, validationContext);
    }

    private void inheritPropertyMethods(Class<?> type, TypeValidationContext validationContext, Map<String, PropertyAnnotationMetadataBuilder> methodBuilders) {
        visitSuperTypes(type, (superType, metadata) -> {
            for (PropertyAnnotationMetadata property : metadata.getPropertiesAnnotationMetadata()) {
                getOrCreatePropertyBuilder(property.getPropertyName(), property.getMethod(), validationContext, methodBuilders)
                    .inheritAnnotations(superType.isInterface(), property);
            }
        });
    }

    private void inheritFunctionMethods(Class<?> type, TypeValidationContext validationContext, Map<MethodSignature, FunctionAnnotationMetadataBuilder> methodBuilders) {
        visitSuperTypes(type, (superType, metadata) -> {
            for (FunctionAnnotationMetadata method : metadata.getFunctionAnnotationMetadata()) {
                getOrCreateFunctionBuilder(method.getMethod(), validationContext, methodBuilders)
                    .inheritAnnotations(superType.isInterface(), method);
            }
        });
    }

    private PropertyAnnotationMetadataBuilder getOrCreatePropertyBuilder(String propertyName, Method getter, TypeValidationContext validationContext, Map<String, PropertyAnnotationMetadataBuilder> propertyBuilders) {
        return propertyBuilders.computeIfAbsent(getter.getName(), methodName -> new PropertyAnnotationMetadataBuilder(propertyName, getter, validationContext));
    }

    private FunctionAnnotationMetadataBuilder getOrCreateFunctionBuilder(Method method, TypeValidationContext validationContext, Map<MethodSignature, FunctionAnnotationMetadataBuilder> methodBuilders) {
        return methodBuilders.computeIfAbsent(MethodSignature.of(method), methodName -> new FunctionAnnotationMetadataBuilder(method, validationContext));
    }

    private ImmutableSortedSet<PropertyAnnotationMetadata> extractPropertiesFrom(Class<?> type, Map<String, PropertyAnnotationMetadataBuilder> methodBuilders, TypeValidationContext validationContext) {
        Method[] methods = type.getDeclaredMethods();
        // Make sure getters end up before the setters
        Arrays.sort(methods, comparing(Method::getName));
        for (Method method : methods) {
            processPropertyMethodAnnotations(method, methodBuilders, validationContext);
        }

        ImmutableList<PropertyAnnotationMetadataBuilder> propertyBuilders = convertMethodToPropertyBuilders(methodBuilders);
        ImmutableMap<String, ImmutableMap<Class<? extends Annotation>, Annotation>> fieldAnnotationsByPropertyName = collectFieldAnnotations(type, validationContext);
        return mergePropertiesAndFieldMetadata(type, propertyBuilders, fieldAnnotationsByPropertyName, validationContext);
    }

    private ImmutableSortedSet<FunctionAnnotationMetadata> extractFunctionsFrom(Class<?> type, Map<MethodSignature, FunctionAnnotationMetadataBuilder> methodBuilders, TypeValidationContext validationContext) {
        Method[] methods = type.getDeclaredMethods();
        // Make sure getters end up before the setters
        Arrays.sort(methods, comparing(Method::getName));
        for (Method method : methods) {
            processFunctionMethodAnnotations(method, methodBuilders, validationContext);
        }

        ImmutableSortedSet.Builder<FunctionAnnotationMetadata> methodsMetadataBuilder = ImmutableSortedSet.naturalOrder();
        methodBuilders.values().forEach(metadataBuilder -> methodsMetadataBuilder.add(metadataBuilder.build()));
        return methodsMetadataBuilder.build();
    }

    private static final String REDUNDANT_GETTERS = "REDUNDANT_GETTERS";

    private ImmutableList<PropertyAnnotationMetadataBuilder> convertMethodToPropertyBuilders(Map<String, PropertyAnnotationMetadataBuilder> methodBuilders) {
        Map<String, PropertyAnnotationMetadataBuilder> propertyBuilders = new LinkedHashMap<>();
        List<PropertyAnnotationMetadataBuilder> metadataBuilders = Ordering.<PropertyAnnotationMetadataBuilder>from(
                comparing(metadataBuilder -> metadataBuilder.getMethod().getName()))
            .sortedCopy(methodBuilders.values());
        for (PropertyAnnotationMetadataBuilder metadataBuilder : metadataBuilders) {
            String propertyName = metadataBuilder.getPropertyName();
            PropertyAnnotationMetadataBuilder previouslySeenBuilder = propertyBuilders.putIfAbsent(propertyName, metadataBuilder);
            // Do we have an 'is'-getter as well as a 'get'-getter? (`previouslySeenBuilder` is the 'get'-getter if so, due to previous sorting)
            if (previouslySeenBuilder != null) {
                // It is okay to have redundant generated 'is'-getters
                if (generatedMethodDetector.test(metadataBuilder.getMethod())) {
                    continue;
                }
                // The 'is'-getter is ignored, we can skip it in favor of the 'get'-getter
                if (ignoredMethodAnnotations.stream()
                    .anyMatch(metadataBuilder::hasAnnotation)) {
                    continue;
                }
                // The 'get'-getter was ignored, we can override it with the 'is'`-getter
                if (ignoredMethodAnnotations.stream()
                    .anyMatch(previouslySeenBuilder::hasAnnotation)) {
                    propertyBuilders.put(propertyName, metadataBuilder);
                    continue;
                }
                previouslySeenBuilder.visitPropertyError(problem ->
                    problem
                        .forProperty(propertyName)
                        .id(TextUtil.screamingSnakeToKebabCase(REDUNDANT_GETTERS), "Property has redundant getters", GradleCoreProblemGroup.validation().property()) // TODO (donat) missing test coverage
                        .contextualLabel(
                            String.format(
                                "has redundant getters: '%s()' and '%s()'",
                                previouslySeenBuilder.getMethod().getName(),
                                metadataBuilder.getMethod().getName()
                            )
                        )
                        .documentedAt(userManual("validation_problems", REDUNDANT_GETTERS.toLowerCase(Locale.ROOT)))
                        .details("Boolean property '" + propertyName + "' has both an `is` and a `get` getter")
                        .solution("Remove one of the getters")
                        .solution("Annotate one of the getters with @Internal")
                );
            } else {
                // Warn on Boolean is-getters that are not ignored and do not have a corresponding get-getter
                Method method = metadataBuilder.getMethod();
                if (PropertyAccessorType.of(method) == PropertyAccessorType.IS_GETTER && method.getReturnType() == Boolean.class && ignoredMethodAnnotations.stream().noneMatch(metadataBuilder::hasAnnotation)) {
                    DeprecationLogger.deprecateAction("Declaring '" + propertyName + "' as a property using an 'is-' method with a Boolean type on " + method.getDeclaringClass().getCanonicalName())
                        .withContext("The combination of method name and return type is not consistent with Java Bean property rules.")
                        .withAdvice(String.format(
                            "Add a method named '%s' with the same behavior and mark the old one with @Deprecated and @ReplacedBy, or change the type of '%s.%s' (and the setter) to 'boolean'.",
                            method.getName().replace("is", "get"),
                            method.getDeclaringClass().getCanonicalName(), method.getName()
                        ))
                        .startingWithGradle10("this property will no longer be treated like a property")
                        .withUpgradeGuideSection(8, "groovy_boolean_properties")
                        .nagUser();
                }
            }
        }
        return ImmutableList.copyOf(propertyBuilders.values());
    }

    private ImmutableMap<String, ImmutableMap<Class<? extends Annotation>, Annotation>> collectFieldAnnotations(Class<?> type, TypeValidationContext validationContext) {
        ImmutableMap.Builder<String, ImmutableMap<Class<? extends Annotation>, Annotation>> fieldAnnotationsByPropertyName = ImmutableMap.builder();
        for (Field declaredField : type.getDeclaredFields()) {
            if (declaredField.isSynthetic()) {
                continue;
            }
            fieldAnnotationsByPropertyName.put(declaredField.getName(), collectRelevantAnnotations(declaredField, propertyAnnotationCategories));
            ImmutableMap<Class<? extends Annotation>, Annotation> nonPropertyAnnotations = collectRelevantAnnotations(declaredField, functionAnnotationCategories);
            if (!nonPropertyAnnotations.isEmpty()) {
                // Function method annotations should not be applicable to fields and should throw a compile time error, but in the
                // event that they are marked applicable to fields, we report them as a problem.  If we add an annotation that is somehow
                // valid in this context, we'll need to handle it in some way to avoid the problem being generated.
                validationContext.visitTypeError(problem ->
                    problem.withAnnotationType(declaredField.getDeclaringClass())
                        .id(TextUtil.screamingSnakeToKebabCase(IGNORED_ANNOTATIONS_ON_PROPERTY), "Ignored annotations on property", GradleCoreProblemGroup.validation().type())
                        .contextualLabel(
                            String.format(
                                "field '%s()' should not be annotated with: %s",
                                declaredField.getName(),
                                simpleAnnotationNames(nonPropertyAnnotations.keySet().stream())
                            )
                        )
                        .documentedAt(userManual("validation_problems", IGNORED_ANNOTATIONS_ON_PROPERTY.toLowerCase(Locale.ROOT)))
                        .details("Function annotations are ignored if they are placed on a field")
                        .solution("Remove the annotations")
                );
            }
        }
        return fieldAnnotationsByPropertyName.build();
    }

    private static final String IGNORED_ANNOTATIONS_ON_FIELD = "IGNORED_ANNOTATIONS_ON_FIELD";

    private ImmutableSortedSet<PropertyAnnotationMetadata> mergePropertiesAndFieldMetadata(Class<?> type, ImmutableList<PropertyAnnotationMetadataBuilder> propertyBuilders, ImmutableMap<String, ImmutableMap<Class<? extends Annotation>, Annotation>> fieldAnnotationsByPropertyName, TypeValidationContext validationContext) {
        ImmutableSortedSet.Builder<PropertyAnnotationMetadata> propertiesMetadataBuilder = ImmutableSortedSet.naturalOrder();
        ImmutableSet.Builder<String> fieldsSeenBuilder = ImmutableSet.builderWithExpectedSize(fieldAnnotationsByPropertyName.size());
        for (PropertyAnnotationMetadataBuilder metadataBuilder : propertyBuilders) {
            String propertyName = metadataBuilder.getPropertyName();
            ImmutableMap<Class<? extends Annotation>, Annotation> fieldAnnotations = fieldAnnotationsByPropertyName.get(propertyName);
            if (fieldAnnotations != null) {
                fieldsSeenBuilder.add(propertyName);
                for (Annotation annotation : fieldAnnotations.values()) {
                    metadataBuilder.declareAnnotation(annotation);
                }
            }
            propertiesMetadataBuilder.add(metadataBuilder.build());
        }
        ImmutableSortedSet<PropertyAnnotationMetadata> propertiesMetadata = propertiesMetadataBuilder.build();

        // Report fields with annotations that have not been seen while processing properties
        ImmutableSet<String> fieldsSeen = fieldsSeenBuilder.build();
        if (fieldsSeen.size() != fieldAnnotationsByPropertyName.size()) {
            fieldAnnotationsByPropertyName.entrySet().stream()
                .filter(entry -> {
                    String fieldName = entry.getKey();
                    ImmutableMap<Class<? extends Annotation>, Annotation> fieldAnnotations = entry.getValue();
                    return !fieldAnnotations.isEmpty()
                        && !fieldsSeen.contains(fieldName)
                        // @Inject is allowed on fields only
                        && !fieldAnnotations.containsKey(Inject.class);
                })
                .forEach(entry -> {
                    String fieldName = entry.getKey();
                    ImmutableMap<Class<? extends Annotation>, Annotation> fieldAnnotations = entry.getValue();
                    validationContext.visitTypeError(problem ->
                        problem
                            .withAnnotationType(type)
                            .id(TextUtil.screamingSnakeToKebabCase(IGNORED_ANNOTATIONS_ON_FIELD), "Incorrect annotations on field", GradleCoreProblemGroup.validation().property()) // TODO (donat) missing test coverage
                            .contextualLabel(
                                String.format(
                                    "field '%s' without corresponding getter has been annotated with %s",
                                    fieldName,
                                    simpleAnnotationNames(fieldAnnotations.keySet().stream())
                                )
                            )
                            .documentedAt(userManual("validation_problems", IGNORED_ANNOTATIONS_ON_FIELD.toLowerCase(Locale.ROOT)))
                            .details("Annotations on fields are only used if there's a corresponding getter for the field")
                            .solution("Add a getter for field '" + fieldName + "'")
                            .solution("Remove the annotations on '" + fieldName + "'")
                    );
                });
        }
        return propertiesMetadata;
    }

    private boolean shouldIgnore(Method method) {
        if (method.isSynthetic()) {
            return true;
        }
        if (method.isBridge()) {
            return true;
        }
        if (isIgnoredGeneratedGroovyMethod(method)) {
            return true;
        }
        return false;
    }

    /**
     * Groovy getters are annotated with `@Generated` but shouldn't be ignored.
     * Other methods which have '$' in their name are usually a good heuristic for the
     * fact we should ignore them, for example methods generated by the @Memoized annotation
     * ref: https://github.com/gradle/gradle/issues/16117
     *
     * @param method a candidate method
     * @return true if we suspect this method to be a safely ignorable generated method
     */
    private boolean isIgnoredGeneratedGroovyMethod(Method method) {
        return generatedMethodDetector.test(method) && method.getName().contains("$");
    }

    private static final String PRIVATE_GETTER_MUST_NOT_BE_ANNOTATED = "PRIVATE_GETTER_MUST_NOT_BE_ANNOTATED";

    private void processPropertyMethodAnnotations(Method method, Map<String, PropertyAnnotationMetadataBuilder> propertyBuilders, TypeValidationContext validationContext) {
        if (shouldIgnore(method)) {
            return;
        }
        // As an optimization first check if the method name is among the candidates before we construct an equivalence wrapper
        if (potentiallyIgnoredMethodNames.contains(method.getName())
            && globallyIgnoredMethods.contains(SIGNATURE_EQUIVALENCE.wrap(method))) {
            return;
        }

        ImmutableMap<Class<? extends Annotation>, Annotation> annotations = collectRelevantAnnotations(method, propertyAnnotationCategories);

        if (Modifier.isStatic(method.getModifiers())) {
            validateNotAnnotatedForProperty(MethodKind.STATIC, method, annotations.keySet(), validationContext);
            return;
        }

        PropertyAccessorType accessorType = PropertyAccessorType.of(method);
        if (accessorType == null) {
            validateNotAnnotatedForProperty(MethodKind.FUNCTION, method, annotations.keySet(), validationContext);
            return;
        }

        String propertyName = accessorType.propertyNameFor(method);
        if (accessorType == PropertyAccessorType.SETTER) {
            validateNotAnnotatedForProperty(MethodKind.SETTER, method, annotations.keySet(), validationContext);
            validateSetterForMutableType(method, accessorType, validationContext, propertyName);
            return;
        }

        // After this point we only see getters

        // Ignore private getters without annotations
        boolean privateGetter = Modifier.isPrivate(method.getModifiers());
        if (privateGetter && annotations.isEmpty()) {
            return;
        }

        PropertyAnnotationMetadataBuilder metadataBuilder = getOrCreatePropertyBuilder(propertyName, method, validationContext, propertyBuilders);
        metadataBuilder.overrideMethod(method);

        if (privateGetter) {
            // At this point we must have annotations on this private getter
            metadataBuilder.visitPropertyError(problem ->
                problem
                    .forProperty(propertyName)
                    .id(TextUtil.screamingSnakeToKebabCase(PRIVATE_GETTER_MUST_NOT_BE_ANNOTATED), "Private property with wrong annotation", GradleCoreProblemGroup.validation().property())
                    .contextualLabel(String.format("is private and annotated with %s", simpleAnnotationNames(annotations.keySet().stream())))
                    .documentedAt(userManual("validation_problems", PRIVATE_GETTER_MUST_NOT_BE_ANNOTATED.toLowerCase(Locale.ROOT)))
                    .details("Annotations on private getters are ignored")
                    .solution("Make the getter public")
                    .solution("Annotate the public version of the getter")
            );
        }

        for (Annotation annotation : annotations.values()) {
            metadataBuilder.declareAnnotation(annotation);
        }
    }

    private static final String PRIVATE_METHOD_MUST_NOT_BE_ANNOTATED = "PRIVATE_METHOD_MUST_NOT_BE_ANNOTATED";

    private void processFunctionMethodAnnotations(Method method, Map<MethodSignature, FunctionAnnotationMetadataBuilder> functionBuilders, TypeValidationContext validationContext) {
        if (shouldIgnore(method)) {
            return;
        }
        // As an optimization first check if the method name is among the candidates before we construct an equivalence wrapper
        if (potentiallyIgnoredMethodNames.contains(method.getName())
            && globallyIgnoredMethods.contains(SIGNATURE_EQUIVALENCE.wrap(method))) {
            return;
        }

        ImmutableMap<Class<? extends Annotation>, Annotation> annotations = collectRelevantAnnotations(method, functionAnnotationCategories);

        // If the function method is not annotated, we can ignore it
        if (annotations.isEmpty()) {
            return;
        }

        if (Modifier.isStatic(method.getModifiers())) {
            validateNotAnnotatedForStaticFunction(method, annotations.keySet(), validationContext);
            return;
        }

        PropertyAccessorType accessorType = PropertyAccessorType.of(method);
        if (accessorType != null) {
            validateNotAnnotatedForPropertyGetter(method, annotations.keySet(), validationContext);
            return;
        }

        FunctionAnnotationMetadataBuilder metadataBuilder = getOrCreateFunctionBuilder(method, validationContext, functionBuilders);
        metadataBuilder.overrideMethod(method);

        boolean privateMethod = Modifier.isPrivate(method.getModifiers());
        if (privateMethod) {
            // At this point we must have annotations on this private getter
            metadataBuilder.visitFunctionProblem(problem ->
                problem
                    .forFunction(method.getName())
                    .id(TextUtil.screamingSnakeToKebabCase(PRIVATE_METHOD_MUST_NOT_BE_ANNOTATED), "Private method with wrong annotation", GradleCoreProblemGroup.validation().property())
                    .contextualLabel(String.format("is private and annotated with %s", simpleAnnotationNames(annotations.keySet().stream())))
                    .documentedAt(userManual("validation_problems", PRIVATE_METHOD_MUST_NOT_BE_ANNOTATED.toLowerCase(Locale.ROOT)))
                    .details("Annotations on private methods are ignored")
                    .solution("Make the method public")
                    .solution("Annotate the public version of the method")
            );
        }

        for (Annotation annotation : annotations.values()) {
            metadataBuilder.declareAnnotation(annotation);
        }
    }

    private static final String MUTABLE_TYPE_WITH_SETTER = "MUTABLE_TYPE_WITH_SETTER";

    private void validateSetterForMutableType(Method setterMethod, PropertyAccessorType setterAccessorType, TypeValidationContext validationContext, String propertyName) {
        Class<?> setterType = setterAccessorType.propertyTypeFor(setterMethod);
        if (isSetterProhibitedForType(setterType)) {
            validationContext.visitPropertyError(problem ->
                problem
                    .forProperty(propertyName)
                    .id(TextUtil.screamingSnakeToKebabCase(MUTABLE_TYPE_WITH_SETTER), "Mutable type with setter", GradleCoreProblemGroup.validation().property())
                    .contextualLabel(String.format("of mutable type '%s' is writable", setterType.getName()))
                    .documentedAt(userManual("validation_problems", MUTABLE_TYPE_WITH_SETTER.toLowerCase(Locale.ROOT)))
                    .details("Properties of type '" + setterType.getName() + "' are already mutable")
                    .solution("Remove the '" + setterMethod.getName() + "' method")
            );
        }
    }

    private boolean isSetterProhibitedForType(Class<?> setter) {
        return mutableNonFinalClasses.stream()
            .anyMatch(prohibited -> prohibited.isAssignableFrom(setter));
    }

    private void visitSuperTypes(Class<?> type, TypeAnnotationMetadataVisitor visitor) {
        Arrays.stream(type.getInterfaces())
            .forEach(superInterface -> visitor.visitType(superInterface, getTypeAnnotationMetadata(superInterface)));
        Class<?> superclass = type.getSuperclass();
        if (superclass != null) {
            visitor.visitType(superclass, getTypeAnnotationMetadata(superclass));
        }
    }

    @FunctionalInterface
    private interface TypeAnnotationMetadataVisitor {
        void visitType(Class<?> type, TypeAnnotationMetadata metadata);
    }

    private static final String IGNORED_ANNOTATIONS_ON_METHOD = "IGNORED_ANNOTATIONS_ON_METHOD";

    private static void validateNotAnnotatedForProperty(MethodKind methodKind, Method method, Set<Class<? extends Annotation>> annotationTypes, TypeValidationContext validationContext) {
        if (!annotationTypes.isEmpty()) {
            validationContext.visitTypeError(problem ->
                problem.withAnnotationType(method.getDeclaringClass())
                    .id(TextUtil.screamingSnakeToKebabCase(IGNORED_ANNOTATIONS_ON_METHOD), "Ignored annotations on method", GradleCoreProblemGroup.validation().type())
                    .contextualLabel(
                        String.format(
                            "%s '%s()' should not be annotated with: %s",
                            methodKind.getDisplayName(), method.getName(),
                            simpleAnnotationNames(annotationTypes.stream())
                        )
                    )
                    .documentedAt(userManual("validation_problems", IGNORED_ANNOTATIONS_ON_METHOD.toLowerCase(Locale.ROOT)))
                    .details("Input/Output annotations are ignored if they are placed on something else than a getter")
                    .solution("Remove the annotations")
                    .solution("Rename the method")
            );
        }
    }

    private static final String IGNORED_ANNOTATIONS_ON_PROPERTY = "IGNORED_ANNOTATIONS_ON_PROPERTY";

    private static void validateNotAnnotatedForPropertyGetter(Method method, Set<Class<? extends Annotation>> annotationTypes, TypeValidationContext validationContext) {
        if (!annotationTypes.isEmpty()) {
            validationContext.visitTypeError(problem ->
                problem.withAnnotationType(method.getDeclaringClass())
                    .id(TextUtil.screamingSnakeToKebabCase(IGNORED_ANNOTATIONS_ON_PROPERTY), "Ignored annotations on property", GradleCoreProblemGroup.validation().type())
                    .contextualLabel(
                        String.format(
                            "%s '%s()' should not be annotated with: %s",
                            MethodKind.PROPERTY.getDisplayName(), method.getName(),
                            simpleAnnotationNames(annotationTypes.stream())
                        )
                    )
                    .documentedAt(userManual("validation_problems", IGNORED_ANNOTATIONS_ON_PROPERTY.toLowerCase(Locale.ROOT)))
                    .details("Function annotations are ignored if they are placed on a property getter")
                    .solution("Remove the annotations")
                    .solution("Rename the method")
            );
        }
    }

    private static void validateNotAnnotatedForStaticFunction(Method method, Set<Class<? extends Annotation>> annotationTypes, TypeValidationContext validationContext) {
        if (!annotationTypes.isEmpty()) {
            validationContext.visitTypeError(problem ->
                problem.withAnnotationType(method.getDeclaringClass())
                    .id(TextUtil.screamingSnakeToKebabCase(IGNORED_ANNOTATIONS_ON_PROPERTY), "Ignored annotations on property", GradleCoreProblemGroup.validation().type())
                    .contextualLabel(
                        String.format(
                            "%s '%s()' should not be annotated with: %s",
                            MethodKind.STATIC.getDisplayName(), method.getName(),
                            simpleAnnotationNames(annotationTypes.stream())
                        )
                    )
                    .documentedAt(userManual("validation_problems", IGNORED_ANNOTATIONS_ON_PROPERTY.toLowerCase(Locale.ROOT)))
                    .details("Function annotations are ignored if they are placed on a static method")
                    .solution("Remove the annotations")
                    .solution("Make the method non-static")
            );
        }
    }

    private static String simpleAnnotationNames(Stream<Class<? extends Annotation>> annotationTypes) {
        return annotationTypes
            .map(annotationType -> "@" + annotationType.getSimpleName())
            .collect(joining(", "));
    }

    private static ImmutableMap<Class<? extends Annotation>, Annotation> collectRelevantAnnotations(AnnotatedElement element, ImmutableMap<Class<? extends Annotation>, AnnotationCategory> relevantCategories) {
        Annotation[] annotations = element.getDeclaredAnnotations();
        if (annotations.length == 0) {
            return ImmutableMap.of();
        }
        ImmutableMap.Builder<Class<? extends Annotation>, Annotation> relevantAnnotations = ImmutableMap.builderWithExpectedSize(annotations.length);
        for (Annotation annotation : annotations) {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            if (relevantCategories.containsKey(annotationType)) {
                relevantAnnotations.put(annotationType, annotation);
            }
        }
        return relevantAnnotations.build();
    }

    private static abstract class HasAnnotationMetadataBuilder {
        protected static final String IGNORED_PROPERTY_MUST_NOT_BE_ANNOTATED = "IGNORED_PROPERTY_MUST_NOT_BE_ANNOTATED";
        protected static final String CONFLICTING_ANNOTATIONS = "CONFLICTING_ANNOTATIONS";
        protected final ListMultimap<AnnotationCategory, Annotation> declaredAnnotations = MultimapBuilder
            .treeKeys(comparing(AnnotationCategory::getDisplayName))
            .arrayListValues()
            .build();
        protected final SetMultimap<AnnotationCategory, Annotation> inheritedInterfaceAnnotations = MultimapBuilder
            .treeKeys(comparing(AnnotationCategory::getDisplayName))
            .linkedHashSetValues()
            .build();
        protected final SetMultimap<AnnotationCategory, Annotation> inheritedSuperclassAnnotations = MultimapBuilder
            .treeKeys(comparing(AnnotationCategory::getDisplayName))
            .linkedHashSetValues()
            .build();
        protected final TypeValidationContext validationContext;
        protected Method method;

        private HasAnnotationMetadataBuilder(Method method, TypeValidationContext validationContext) {
            overrideMethod(method);
            this.validationContext = validationContext;
        }

        public Method getMethod() {
            return method;
        }

        public void overrideMethod(Method method) {
            this.method = method;
        }

        public void declareAnnotation(Annotation annotation) {
            AnnotationCategory category = geAnnotationCategories().get(annotation.annotationType());
            declaredAnnotations.put(category, annotation);
        }

        public void inheritAnnotations(boolean fromInterface, HasAnnotationMetadata superProperty) {
            superProperty.getAnnotationsByCategory()
                .forEach((fromInterface
                    ? inheritedInterfaceAnnotations
                    : inheritedSuperclassAnnotations)::put);
        }

        protected ImmutableSet<AnnotationCategory> allAnnotationCategories() {
            return ImmutableSet.<AnnotationCategory>builder()
                .addAll(declaredAnnotations.keySet())
                .addAll(inheritedInterfaceAnnotations.keySet())
                .addAll(inheritedSuperclassAnnotations.keySet())
                .build();
        }

        public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
            Iterable<Annotation> allAnnotations = Iterables.concat(
                declaredAnnotations.values(),
                inheritedInterfaceAnnotations.values(),
                inheritedSuperclassAnnotations.values()
            );
            for (Annotation annotation : allAnnotations) {
                if (annotation.annotationType().equals(annotationType)) {
                    return true;
                }
            }
            return false;
        }

        protected ImmutableMap<AnnotationCategory, Annotation> resolveAnnotations() {
            ImmutableMap.Builder<AnnotationCategory, Annotation> builder = ImmutableMap.builder();
            for (AnnotationCategory category : allAnnotationCategories()) {
                Annotation resolvedAnnotation;
                Collection<Annotation> declaredAnnotationsForCategory = declaredAnnotations.get(category);
                if (!declaredAnnotationsForCategory.isEmpty()) {
                    resolvedAnnotation = resolveAnnotation("declared", category, declaredAnnotationsForCategory);
                } else {
                    Collection<Annotation> interfaceAnnotations = inheritedInterfaceAnnotations.get(category);
                    if (!interfaceAnnotations.isEmpty()) {
                        resolvedAnnotation = resolveAnnotation("inherited (from interface)", category, interfaceAnnotations);
                    } else {
                        Collection<Annotation> superclassAnnotations = inheritedSuperclassAnnotations.get(category);
                        resolvedAnnotation = resolveAnnotation("inherited (from superclass)", category, superclassAnnotations);
                    }
                }
                builder.put(category, resolvedAnnotation);
            }
            return builder.build();
        }

        private Annotation resolveAnnotation(String source, AnnotationCategory category, Collection<Annotation> annotationsForCategory) {
            Iterator<Annotation> declaredAnnotationForCategoryIterator = annotationsForCategory.iterator();
            // Ignore all but the first recorded annotation
            Annotation declaredAnnotationForCategory = declaredAnnotationForCategoryIterator.next();
            if (declaredAnnotationForCategoryIterator.hasNext()) {
                handleConflictingAnnotation(source, category, annotationsForCategory);
            }
            return declaredAnnotationForCategory;
        }

        abstract protected ImmutableMap<Class<? extends Annotation>, AnnotationCategory> geAnnotationCategories();

        abstract protected void handleConflictingAnnotation(String source, AnnotationCategory category, Collection<Annotation> annotationsForCategory);
    }

    private class PropertyAnnotationMetadataBuilder extends HasAnnotationMetadataBuilder implements Comparable<PropertyAnnotationMetadataBuilder> {
        private final String propertyName;

        public PropertyAnnotationMetadataBuilder(String propertyName, Method getter, TypeValidationContext validationContext) {
            super(getter, validationContext);
            this.propertyName = propertyName;
        }

        public String getPropertyName() {
            return propertyName;
        }

        @Override
        protected ImmutableMap<Class<? extends Annotation>, AnnotationCategory> geAnnotationCategories() {
            return propertyAnnotationCategories;
        }

        public PropertyAnnotationMetadata build() {
            return new DefaultPropertyAnnotationMetadata(propertyName, getMethod(), resolveAnnotations());
        }

        private void visitPropertyError(Action<? super TypeAwareProblemBuilder> problemSpec) {
            validationContext.visitPropertyError(problemSpec);
        }

        @Override
        protected ImmutableMap<AnnotationCategory, Annotation> resolveAnnotations() {
            // If method should be ignored, then ignore all other annotations
            List<Annotation> declaredTypes = declaredAnnotations.get(TYPE);
            for (Annotation declaredType : declaredTypes) {
                Class<? extends Annotation> ignoredMethodAnnotation = declaredType.annotationType();
                if (ignoredMethodAnnotations.contains(ignoredMethodAnnotation)) {
                    if (declaredAnnotations.values().size() > 1 && ignoreAnnotationDisallowedModifiers(declaredAnnotations.values()).count() > 1) {
                        handleAnnotatedIgnoredMethod(ignoredMethodAnnotation);
                    }
                    return ImmutableMap.of(TYPE, declaredType);
                }
            }

            return super.resolveAnnotations();
        }

        private Stream<Annotation> ignoreAnnotationDisallowedModifiers(Collection<Annotation> annotations) {
            return annotations.stream().filter(annotation -> !ignoredMethodAnnotationsAllowedModifiers.contains(annotation.annotationType()));
        }

        private void handleAnnotatedIgnoredMethod(Class<? extends Annotation> ignoredMethodAnnotation) {
            visitPropertyError(problem ->
                problem
                    .forProperty(propertyName)
                    .id(TextUtil.screamingSnakeToKebabCase(IGNORED_PROPERTY_MUST_NOT_BE_ANNOTATED), "Has wrong combination of annotations", GradleCoreProblemGroup.validation().property())
                    .contextualLabel(
                        String.format(
                            "annotated with @%s should not be also annotated with %s",
                            ignoredMethodAnnotation.getSimpleName(),
                            simpleAnnotationNames(ignoreAnnotationDisallowedModifiers(declaredAnnotations.values())
                                .<Class<? extends Annotation>>map(Annotation::annotationType)
                                .filter(annotationType -> !annotationType.equals(ignoredMethodAnnotation)))
                        )
                    )
                    .documentedAt(userManual("validation_problems", IGNORED_PROPERTY_MUST_NOT_BE_ANNOTATED.toLowerCase(Locale.ROOT)))
                    .details("A property is ignored but also has input annotations")
                    .solution("Remove the input annotations")
                    .solution("Remove the @" + ignoredMethodAnnotation.getSimpleName() + " annotation")
            );
        }

        @Override
        protected void handleConflictingAnnotation(String source, AnnotationCategory category, Collection<Annotation> annotationsForCategory) {
            visitPropertyError(problem ->
                problem
                    .forProperty(propertyName)
                    .id(TextUtil.screamingSnakeToKebabCase(CONFLICTING_ANNOTATIONS), StringUtils.capitalize(category.getDisplayName()) + " has conflicting annotation", GradleCoreProblemGroup.validation().property())
                    .contextualLabel(
                        String.format(
                            "has conflicting %s annotations %s: %s",
                            category.getDisplayName(),
                            source,
                            simpleAnnotationNames(annotationsForCategory.stream().map(Annotation::annotationType))
                        )
                    )
                    .documentedAt(userManual("validation_problems", CONFLICTING_ANNOTATIONS.toLowerCase(Locale.ROOT)))
                    .details("The different annotations have different semantics and Gradle cannot determine which one to pick")
                    .solution("Choose between one of the conflicting annotations")
            );
        }

        @Override
        public int compareTo(PropertyAnnotationMetadataBuilder o) {
            return propertyName.compareTo(o.propertyName);
        }
    }

    private class FunctionAnnotationMetadataBuilder extends HasAnnotationMetadataBuilder implements Comparable<FunctionAnnotationMetadataBuilder> {
        public FunctionAnnotationMetadataBuilder(Method method, TypeValidationContext validationContext) {
            super(method, validationContext);
        }

        @Override
        protected ImmutableMap<Class<? extends Annotation>, AnnotationCategory> geAnnotationCategories() {
            return functionAnnotationCategories;
        }

        public FunctionAnnotationMetadata build() {
            return new DefaultFunctionAnnotationMetadata(getMethod(), resolveAnnotations());
        }

        private void visitFunctionProblem(Action<? super TypeAwareProblemBuilder> problemSpec) {
            validationContext.visitTypeError(problemSpec);
        }

        @Override
        protected void handleConflictingAnnotation(String source, AnnotationCategory category, Collection<Annotation> annotationsForCategory) {
            visitFunctionProblem(problem ->
                problem
                    .forFunction(getMethod().getName())
                    .id(TextUtil.screamingSnakeToKebabCase(CONFLICTING_ANNOTATIONS), StringUtils.capitalize(category.getDisplayName()) + " has conflicting annotation", GradleCoreProblemGroup.validation().type())
                    .contextualLabel(
                        String.format(
                            "has conflicting %s annotations %s: %s",
                            category.getDisplayName(),
                            source,
                            simpleAnnotationNames(annotationsForCategory.stream().map(Annotation::annotationType))
                        )
                    )
                    .documentedAt(userManual("validation_problems", CONFLICTING_ANNOTATIONS.toLowerCase(Locale.ROOT)))
                    .details("The different annotations have different semantics and Gradle cannot determine which one to pick")
                    .solution("Choose between one of the conflicting annotations")
            );
        }

        @Override
        public int compareTo(FunctionAnnotationMetadataBuilder o) {
            return getMethod().getName().compareTo(o.getMethod().getName());
        }
    }

    private enum MethodKind {
        STATIC("static method"),
        PROPERTY("property"),
        FUNCTION("function"),
        SETTER("setter");

        private final String displayName;

        MethodKind(String displayName) {
            this.displayName = displayName;
        }

        public String getDisplayName() {
            return displayName;
        }
    }

    private static class MethodSignature {
        private final String name;
        private final Class<?>[] parameterTypes;

        public MethodSignature(String name, Class<?>[] parameterTypes) {
            this.name = name;
            this.parameterTypes = parameterTypes;
        }

        public static MethodSignature of(Method method) {
            return new MethodSignature(method.getName(), method.getParameterTypes());
        }

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

            MethodSignature that = (MethodSignature) o;
            return name.equals(that.name) && Arrays.equals(parameterTypes, that.parameterTypes);
        }

        @Override
        public int hashCode() {
            int result = name.hashCode();
            result = 31 * result + Arrays.hashCode(parameterTypes);
            return result;
        }
    }
}
