/*
 * 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.instantiation.generator;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;
import groovy.lang.Closure;
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.gradle.api.Action;
import org.gradle.api.Describable;
import org.gradle.api.DomainObjectSet;
import org.gradle.api.ExtensiblePolymorphicDomainObjectContainer;
import org.gradle.api.IsolatedAction;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.NonExtensible;
import org.gradle.api.NonNullApi;
import org.gradle.api.artifacts.dsl.DependencyCollector;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.ConfigurableFileTree;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.internal.DynamicObjectAware;
import org.gradle.api.internal.IConventionAware;
import org.gradle.api.internal.plugins.software.SoftwareType;
import org.gradle.api.plugins.ExtensionAware;
import org.gradle.api.provider.HasMultipleValues;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.provider.SupportsConvention;
import org.gradle.api.reflect.InjectionPointQualifier;
import org.gradle.api.tasks.Nested;
import org.gradle.cache.Cache;
import org.gradle.internal.Cast;
import org.gradle.internal.deprecation.DeprecationLogger;
import org.gradle.internal.extensibility.NoConventionMapping;
import org.gradle.internal.instantiation.ClassGenerationException;
import org.gradle.internal.instantiation.InjectAnnotationHandler;
import org.gradle.internal.instantiation.InstanceGenerator;
import org.gradle.internal.instantiation.PropertyRoleAnnotationHandler;
import org.gradle.internal.logging.text.TreeFormatter;
import org.gradle.internal.reflect.ClassDetails;
import org.gradle.internal.reflect.ClassInspector;
import org.gradle.internal.reflect.JavaPropertyReflectionUtil;
import org.gradle.internal.reflect.JavaReflectionUtil;
import org.gradle.internal.reflect.MethodSet;
import org.gradle.internal.reflect.PropertyAccessorType;
import org.gradle.internal.reflect.PropertyDetails;
import org.gradle.internal.service.ServiceLookup;
import org.gradle.internal.service.ServiceRegistry;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Optional.ofNullable;
import static org.gradle.api.internal.GeneratedSubclasses.unpack;

/**
 * Generates a subclass of the target class to mix-in some DSL behaviour.
 *
 * <ul>
 * <li>For each property, a convention mapping is applied. These properties may have a setter method.</li>
 * <li>For each property whose getter is annotated with {@link Inject}, service instance will be injected instead. These properties may have a setter method and may be abstract.</li>
 * <li>For each mutable property as set method is generated.</li>
 * <li>For each method whose last parameter is an {@link org.gradle.api.Action}, an override is generated that accepts a {@link groovy.lang.Closure} instead.</li>
 * <li>Coercion from string to enum property is mixed in.</li>
 * <li>{@link groovy.lang.GroovyObject} and {@link DynamicObjectAware} is mixed in to the class.</li>
 * <li>An {@link ExtensionAware} implementation is added, unless {@link NonExtensible} is attached to the class.</li>
 * <li>An {@link IConventionAware} implementation is added, unless {@link NoConventionMapping} is attached to the class.</li>
 * </ul>
 */
abstract class AbstractClassGenerator implements ClassGenerator {
    /**
     * Types that are allowed to be instantiated directly by Gradle when exposed as a getter on a type.
     *
     * @implNote Keep in sync with platforms/documentation/docs/src/docs/userguide/authoring-builds/gradle-properties/properties_providers.adoc
     * @see ManagedObjectFactory#newInstance
     */
    private static final ImmutableSet<Class<?>> MANAGED_PROPERTY_TYPES = ImmutableSet.of(
        ConfigurableFileCollection.class,
        ConfigurableFileTree.class,
        ListProperty.class,
        SetProperty.class,
        MapProperty.class,
        RegularFileProperty.class,
        DirectoryProperty.class,
        Property.class,
        NamedDomainObjectContainer.class,
        ExtensiblePolymorphicDomainObjectContainer.class,
        DomainObjectSet.class,
        DependencyCollector.class
    );

    private static final ImmutableSet<Class<? extends Annotation>> NESTED_ANNOTATION_TYPES = ImmutableSet.of(
        Nested.class,
        SoftwareType.class
    );

    private static final Object[] NO_PARAMS = new Object[0];

    private final Cache<Class<?>, GeneratedClassImpl> generatedClasses;
    private final ImmutableSet<Class<? extends Annotation>> disabledAnnotations;
    private final ImmutableSet<Class<? extends Annotation>> enabledAnnotations;
    private final ImmutableMultimap<Class<? extends Annotation>, TypeToken<?>> allowedTypesForAnnotation;
    private final PropertyRoleAnnotationHandler roleHandler;

    protected AbstractClassGenerator(
        Collection<? extends InjectAnnotationHandler> allKnownAnnotations,
        Collection<Class<? extends Annotation>> enabledAnnotations,
        PropertyRoleAnnotationHandler roleHandler,
        Cache<Class<?>, GeneratedClassImpl> generatedClassesCache
    ) {
        this.generatedClasses = generatedClassesCache;
        this.enabledAnnotations = ImmutableSet.copyOf(enabledAnnotations);
        ImmutableSet.Builder<Class<? extends Annotation>> builder = ImmutableSet.builder();
        ImmutableListMultimap.Builder<Class<? extends Annotation>, TypeToken<?>> allowedTypesBuilder = ImmutableListMultimap.builder();
        for (InjectAnnotationHandler handler : allKnownAnnotations) {
            Class<? extends Annotation> annotationType = handler.getAnnotationType();
            if (!enabledAnnotations.contains(annotationType)) {
                builder.add(annotationType);
            } else {
                InjectionPointQualifier injectionPointQualifier = annotationType.getAnnotation(InjectionPointQualifier.class);
                if (injectionPointQualifier != null) {
                    for (Class<?> supportedType : injectionPointQualifier.supportedTypes()) {
                        allowedTypesBuilder.put(annotationType, TypeToken.of(supportedType));
                    }
                    for (Class<?> supportedProviderType : injectionPointQualifier.supportedProviderTypes()) {
                        allowedTypesBuilder.put(annotationType, providerOf(supportedProviderType));
                    }
                }
            }
        }
        this.disabledAnnotations = builder.build();
        this.allowedTypesForAnnotation = allowedTypesBuilder.build();
        this.roleHandler = roleHandler;
    }

    PropertyRoleAnnotationHandler getRoleHandler() {
        return roleHandler;
    }

    private static <T> TypeToken<Provider<T>> providerOf(Class<T> providerType) {
        return new TypeToken<Provider<T>>() {
        }.where(new TypeParameter<T>() {
        }, providerType);
    }

    @Override
    public <T> GeneratedClass<? extends T> generate(Class<T> type) {
        return Cast.uncheckedNonnullCast(generatedClasses.get(unpack(type), this::generateUnderLock));
    }

    private GeneratedClassImpl generateUnderLock(Class<?> type) {
        List<CustomInjectAnnotationPropertyHandler> customAnnotationPropertyHandlers = new ArrayList<>(enabledAnnotations.size());

        ServicesPropertyHandler servicesHandler = new ServicesPropertyHandler();
        InjectAnnotationPropertyHandler injectionHandler = new InjectAnnotationPropertyHandler();
        LazyGroovySupportTypePropertyHandler lazyGroovySupportTypedHandler = new LazyGroovySupportTypePropertyHandler();
        ManagedPropertiesHandler managedPropertiesHandler = new ManagedPropertiesHandler();
        NamePropertyHandler namePropertyHandler = new NamePropertyHandler();
        ExtensibleTypePropertyHandler extensibleTypeHandler = new ExtensibleTypePropertyHandler();
        DslMixInPropertyType dslMixInHandler = new DslMixInPropertyType(extensibleTypeHandler);

        // Order is significant. Injection handler should be at the end
        List<ClassGenerationHandler> handlers = new ArrayList<>(5 + enabledAnnotations.size() + disabledAnnotations.size());
        handlers.add(extensibleTypeHandler);
        handlers.add(dslMixInHandler);
        handlers.add(lazyGroovySupportTypedHandler);
        handlers.add(servicesHandler);
        handlers.add(namePropertyHandler);
        handlers.add(managedPropertiesHandler);
        for (Class<? extends Annotation> annotation : enabledAnnotations) {
            customAnnotationPropertyHandlers.add(new CustomInjectAnnotationPropertyHandler(annotation));
        }
        handlers.addAll(customAnnotationPropertyHandlers);
        handlers.add(injectionHandler);

        // Order is significant
        List<ClassValidator> validators = new ArrayList<>(2 + disabledAnnotations.size());
        for (Class<? extends Annotation> annotation : disabledAnnotations) {
            validators.add(new DisabledAnnotationValidator(annotation));
        }
        validators.add(new InjectionAnnotationValidator(enabledAnnotations, allowedTypesForAnnotation));
        validators.add(new BooleanPropertyDeprecatingValidator());

        Class<?> generatedClass;
        try {
            ClassInspectionVisitor inspectionVisitor = start(type);

            inspectType(type, validators, handlers, extensibleTypeHandler);
            for (ClassGenerationHandler handler : handlers) {
                handler.applyTo(inspectionVisitor);
            }

            ClassGenerationVisitor generationVisitor = inspectionVisitor.builder();
            for (ClassGenerationHandler handler : handlers) {
                handler.applyTo(generationVisitor);
            }

            boolean shouldImplementNameProperty = namePropertyHandler.hasNameProperty();
            if (type.isInterface()) {
                if (shouldImplementNameProperty) {
                    generationVisitor.addNameConstructor();
                } else {
                    generationVisitor.addDefaultConstructor();
                }
            } else {
                for (Constructor<?> constructor : type.getConstructors()) {
                    generationVisitor.addConstructor(constructor, shouldImplementNameProperty);
                }
            }

            generatedClass = generationVisitor.generate();
        } catch (ClassGenerationException e) {
            throw e;
        } catch (Throwable e) {
            TreeFormatter formatter = new TreeFormatter();
            formatter.node("Could not generate a decorated class for type ");
            formatter.appendType(type);
            formatter.append(".");
            throw new ClassGenerationException(formatter.toString(), e);
        }

        ImmutableList.Builder<Class<? extends Annotation>> annotationsTriggeringServiceInjection = ImmutableList.builder();
        for (CustomInjectAnnotationPropertyHandler handler : customAnnotationPropertyHandlers) {
            if (handler.isUsed()) {
                annotationsTriggeringServiceInjection.add(handler.getAnnotation());
            }
        }

        // This is expensive to calculate, so cache the result
        Class<?> enclosingClass = type.getEnclosingClass();
        Class<?> outerType;
        if (enclosingClass != null && !Modifier.isStatic(type.getModifiers())) {
            outerType = enclosingClass;
        } else {
            outerType = null;
        }

        return new GeneratedClassImpl(generatedClass, outerType, injectionHandler.getInjectedServices(), annotationsTriggeringServiceInjection.build());
    }

    protected abstract ClassInspectionVisitor start(Class<?> type);

    protected abstract InstantiationStrategy createUsingConstructor(Constructor<?> constructor);

    protected abstract InstantiationStrategy createForSerialization(Class<?> generatedType, Class<?> baseClass);

    private void inspectType(Class<?> type, List<ClassValidator> validators, List<ClassGenerationHandler> generationHandlers, UnclaimedPropertyHandler unclaimedHandler) {
        ClassDetails classDetails = ClassInspector.inspect(type);
        ClassMetadata classMetaData = new ClassMetadata(type);
        assembleProperties(classDetails, classMetaData);

        for (ClassGenerationHandler handler : generationHandlers) {
            handler.startType(type);
        }

        for (Method method : classDetails.getAllMethods()) {
            for (ClassValidator validator : validators) {
                validator.validateMethod(method, PropertyAccessorType.of(method));
            }
        }

        for (PropertyDetails property : classDetails.getProperties()) {
            for (ClassValidator validator : validators) {
                validator.validateProperty(property);
            }

            PropertyMetadata propertyMetaData = classMetaData.property(property.getName());
            for (ClassGenerationHandler handler : generationHandlers) {
                handler.visitProperty(propertyMetaData);
            }

            ClassGenerationHandler claimedBy = null;
            for (ClassGenerationHandler handler : generationHandlers) {
                if (!handler.claimPropertyImplementation(propertyMetaData)) {
                    continue;
                }
                if (claimedBy == null) {
                    claimedBy = handler;
                } else {
                    handler.ambiguous(propertyMetaData);
                    break;
                }
            }
            if (claimedBy != null) {
                continue;
            }

            unclaimedHandler.unclaimed(propertyMetaData);
            for (Method method : property.getGetters()) {
                assertNotAbstract(type, method);
            }
            for (Method method : property.getSetters()) {
                assertNotAbstract(type, method);
            }
            for (Method method : propertyMetaData.setMethods) {
                assertNotAbstract(type, method);
            }
        }

        for (Method method : classDetails.getInstanceMethods()) {
            assertNotAbstract(type, method);
            for (ClassGenerationHandler handler : generationHandlers) {
                handler.visitInstanceMethod(method);
            }
        }

        visitFields(classDetails, generationHandlers);
    }

    private void visitFields(ClassDetails type, List<ClassGenerationHandler> generationHandlers) {
        if (hasRelevantFields(type)) {
            for (ClassGenerationHandler handler : generationHandlers) {
                handler.hasFields();
            }
        }
    }

    private boolean hasRelevantFields(ClassDetails type) {
        List<Field> instanceFields = type.getInstanceFields();
        if (instanceFields.isEmpty()) {
            return false;
        }
        // Ignore irrelevant synthetic metaClass field injected by the Groovy compiler
        return instanceFields.size() != 1
            || !isSyntheticMetaClassField(instanceFields.get(0));
    }

    private boolean isSyntheticMetaClassField(Field field) {
        return field.isSynthetic() && field.getType() == MetaClass.class;
    }

    private void assembleProperties(ClassDetails classDetails, ClassMetadata classMetaData) {
        for (PropertyDetails property : classDetails.getProperties()) {
            PropertyMetadata propertyMetaData = classMetaData.property(property.getName());
            for (Method method : property.getGetters()) {
                propertyMetaData.addGetter(classMetaData.resolveTypeVariables(method));
            }
            for (Method method : property.getSetters()) {
                propertyMetaData.addSetter(method);
            }
            if (property.getBackingField() != null) {
                propertyMetaData.field(property.getBackingField());
            }
        }
        for (Method method : classDetails.getInstanceMethods()) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 1) {
                PropertyMetadata propertyMetaData = classMetaData.getProperty(method.getName());
                if (propertyMetaData != null) {
                    propertyMetaData.addSetMethod(method);
                }
            }
        }
    }

    private void assertNotAbstract(Class<?> type, Method method) {
        if (Modifier.isAbstract(type.getModifiers()) && Modifier.isAbstract(method.getModifiers())) {
            TreeFormatter formatter = new TreeFormatter();
            formatter.node("Cannot have abstract method ");
            formatter.appendMethod(method);
            formatter.append(".");
            throw new IllegalArgumentException(formatter.toString());
        }
        // Else, ignore abstract methods on non-abstract classes as some other tooling (e.g. the Groovy compiler) has decided this is ok
    }

    private static boolean isManagedProperty(PropertyMetadata property) {
        // Property is readable and without a setter of property type and the type can be created
        return property.isReadableWithoutSetterOfPropertyType() && (MANAGED_PROPERTY_TYPES.contains(property.getType()) || hasNestedAnnotation(property));
    }

    private static boolean hasNestedAnnotation(PropertyMetadata property) {
        return NESTED_ANNOTATION_TYPES.stream().anyMatch(property::hasAnnotation);
    }

    private static boolean isEagerAttachProperty(PropertyMetadata property) {
        // Property is readable and without a setter of property type and getter is final, so attach owner eagerly in constructor
        // This should apply to all 'managed' types however for backwards compatibility is applied only to property types
        return property.isReadableWithoutSetterOfPropertyType() && !property.getMainGetter().shouldOverride() && hasPropertyType(property);
    }

    private static boolean isIneligibleForConventionMapping(PropertyMetadata property) {
        // Provider API types and convention-supporting types in general should have conventions set through convention() instead of
        // using convention mapping.
        return Provider.class.isAssignableFrom(property.getType()) || SupportsConvention.class.isAssignableFrom(property.getType());
    }

    private static boolean isLazyAttachProperty(PropertyMetadata property) {
        // Property is readable and without a setter of property type and getter is not final, so attach owner lazily when queried
        // This should apply to all 'managed' types however only the ConfigurableFileCollection and Provider types and @Nested value current implement OwnerAware
        return property.isReadableWithoutSetterOfPropertyType() && !property.getOverridableGetters().isEmpty()
            && (Provider.class.isAssignableFrom(property.getType()) || isConfigurableFileCollectionType(property.getType()) || hasNestedAnnotation(property));
    }

    private static boolean isReattachProperty(PropertyMetadata property) {
        // Properties that should have reattached property owners upon reading from the cache
        return hasPropertyType(property) || isConfigurableFileCollectionType(property.getType());
    }

    private static boolean isNameProperty(PropertyMetadata property) {
        // Property is read only, called "name", has type String and getter is abstract
        return property.isReadOnly() && "name".equals(property.getName()) && property.getType() == String.class && property.getMainGetter().isAbstract();
    }

    private static boolean hasPropertyType(PropertyMetadata property) {
        return isPropertyType(property.getType());
    }

    private static boolean isPropertyType(Class<?> type) {
        return Property.class.isAssignableFrom(type) ||
            HasMultipleValues.class.isAssignableFrom(type) ||
            MapProperty.class.isAssignableFrom(type);
    }

    private static boolean isConfigurableFileCollectionType(Class<?> type) {
        return ConfigurableFileCollection.class.isAssignableFrom(type);
    }

    private static boolean isAttachableType(MethodMetadata method) {
        return Provider.class.isAssignableFrom(method.getReturnType()) || isConfigurableFileCollectionType(method.getReturnType()) || hasNestedAnnotation(method);
    }

    private static boolean hasNestedAnnotation(MethodMetadata method) {
        return NESTED_ANNOTATION_TYPES.stream().anyMatch(annotation -> method.method.getAnnotation(annotation) != null);
    }

    private boolean isRoleType(PropertyMetadata property) {
        for (Class<? extends Annotation> roleAnnotation : roleHandler.getAnnotationTypes()) {
            if (property.hasAnnotation(roleAnnotation)) {
                return true;
            }
        }
        return false;
    }

    protected class GeneratedClassImpl implements GeneratedClass<Object> {
        private final Class<?> generatedClass;
        private final Class<?> outerType;
        private final List<Class<?>> injectedServices;
        private final List<Class<? extends Annotation>> annotationsTriggeringServiceInjection;
        private final List<GeneratedConstructor<Object>> constructors;

        public GeneratedClassImpl(Class<?> generatedClass, @Nullable Class<?> outerType, List<Class<?>> injectedServices, List<Class<? extends Annotation>> annotationsTriggeringServiceInjection) {
            this.generatedClass = generatedClass;
            this.outerType = outerType;
            this.injectedServices = injectedServices;
            this.annotationsTriggeringServiceInjection = annotationsTriggeringServiceInjection;

            ImmutableList.Builder<GeneratedConstructor<Object>> builder = ImmutableList.builderWithExpectedSize(generatedClass.getDeclaredConstructors().length);
            for (final Constructor<?> constructor : generatedClass.getDeclaredConstructors()) {
                if (!constructor.isSynthetic()) {
                    constructor.setAccessible(true);
                    builder.add(new GeneratedConstructorImpl(constructor));
                }
            }
            this.constructors = Ordering.from(new ConstructorComparator()).sortedCopy(builder.build());
        }

        @Override
        public Class<Object> getGeneratedClass() {
            return Cast.uncheckedNonnullCast(generatedClass);
        }

        @Nullable
        @Override
        public Class<?> getOuterType() {
            return outerType;
        }

        @Override
        public List<GeneratedConstructor<Object>> getConstructors() {
            return constructors;
        }

        @Override
        public SerializationConstructor<Object> getSerializationConstructor(Class<? super Object> baseClass) {
            return new SerializationConstructorImpl(baseClass);
        }

        private class SerializationConstructorImpl implements SerializationConstructor<Object> {
            private final InstantiationStrategy strategy;

            public SerializationConstructorImpl(Class<?> baseClass) {
                this.strategy = createForSerialization(generatedClass, baseClass);
            }

            @Override
            public Object newInstance(ServiceLookup services, InstanceGenerator nested) throws InvocationTargetException, IllegalAccessException, InstantiationException {
                return strategy.newInstance(services, nested, null, NO_PARAMS);
            }
        }

        private class GeneratedConstructorImpl implements GeneratedConstructor<Object> {
            private final Constructor<?> constructor;
            private final InstantiationStrategy strategy;

            public GeneratedConstructorImpl(Constructor<?> constructor) {
                this.constructor = constructor;
                this.strategy = createUsingConstructor(constructor);
            }

            @Override
            public Object newInstance(ServiceLookup services, InstanceGenerator nested, @Nullable Describable displayName, Object[] params) throws InvocationTargetException, IllegalAccessException, InstantiationException {
                return strategy.newInstance(services, nested, displayName, params);
            }

            @Override
            public boolean requiresService(Class<?> serviceType) {
                for (Class<?> parameterType : constructor.getParameterTypes()) {
                    if (parameterType.isAssignableFrom(serviceType)) {
                        return true;
                    }
                }
                for (Class<?> injectedService : injectedServices) {
                    if (injectedService.isAssignableFrom(serviceType)) {
                        return true;
                    }
                }
                return false;
            }

            @Override
            public boolean serviceInjectionTriggeredByAnnotation(Class<? extends Annotation> serviceAnnotation) {
                return annotationsTriggeringServiceInjection.contains(serviceAnnotation);
            }

            @Override
            public Class<?>[] getParameterTypes() {
                return constructor.getParameterTypes();
            }

            @Override
            public Type[] getGenericParameterTypes() {
                return constructor.getGenericParameterTypes();
            }

            @Nullable
            @Override
            public <S extends Annotation> S getAnnotation(Class<S> annotation) {
                return constructor.getAnnotation(annotation);
            }

            @Override
            public int getModifiers() {
                return constructor.getModifiers();
            }
        }
    }

    private static class ClassMetadata {
        private final Class<?> type;
        private final Map<String, PropertyMetadata> properties = new LinkedHashMap<>();

        public ClassMetadata(Class<?> type) {
            this.type = type;
        }

        /**
         * Determines the concrete return type of the given method, resolving any type parameters.
         */
        public MethodMetadata resolveTypeVariables(Method method) {
            Type resolvedReturnType = JavaPropertyReflectionUtil.resolveMethodReturnType(type, method);
            return new MethodMetadata(method, resolvedReturnType);
        }

        @Nullable
        public PropertyMetadata getProperty(String name) {
            return properties.get(name);
        }

        public PropertyMetadata property(String name) {
            PropertyMetadata property = properties.get(name);
            if (property == null) {
                property = new PropertyMetadata(name);
                properties.put(name, property);
            }
            return property;
        }
    }

    protected static class MethodMetadata {
        private final Method method;
        private final Type returnType;

        public MethodMetadata(Method method, Type returnType) {
            this.method = method;
            this.returnType = returnType;
        }

        public String getName() {
            return method.getName();
        }

        public boolean isAbstract() {
            return Modifier.isAbstract(method.getModifiers());
        }

        boolean shouldOverride() {
            return !Modifier.isFinal(method.getModifiers()) && !method.isBridge();
        }

        boolean shouldImplement() {
            return !method.isBridge();
        }

        public Class<?> getReturnType() {
            return method.getReturnType();
        }

        public Type getGenericReturnType() {
            return returnType;
        }
    }

    protected static class PropertyMetadata {
        private final String name;
        private final List<MethodMetadata> getters = new ArrayList<>();
        private final List<MethodMetadata> overridableGetters = new ArrayList<>();
        private final List<Method> overridableSetters = new ArrayList<>();
        private final List<Method> setters = new ArrayList<>();
        private final List<Method> setMethods = new ArrayList<>();
        private MethodMetadata mainGetter;
        private Field backingField;

        private PropertyMetadata(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "[property " + name + "]";
        }

        public String getName() {
            return name;
        }

        public boolean isReadOnly() {
            return isReadable() && !isWritable();
        }

        public boolean isReadableWithoutSetterOfPropertyType() {
            return isReadable() && setters.stream().noneMatch(method -> method.getParameterTypes()[0].equals(getType()));
        }

        public boolean isReadable() {
            return mainGetter != null;
        }

        public boolean isWritable() {
            return !setters.isEmpty();
        }

        public boolean hasAnnotation(Class<? extends Annotation> type) {
            return findAnnotation(type) != null;
        }

        @Nullable
        public <A extends Annotation> A findAnnotation(Class<A> type) {
            return ofNullable(backingFieldAnnotationOf(type)).orElseGet(() -> getterAnnotationOf(type));
        }

        private <A extends Annotation> A getterAnnotationOf(Class<A> type) {
            return mainGetter.method.getAnnotation(type);
        }

        private <A extends Annotation> A backingFieldAnnotationOf(Class<A> type) {
            return backingField == null ? null : backingField.getAnnotation(type);
        }

        public MethodMetadata getMainGetter() {
            return mainGetter;
        }

        public List<MethodMetadata> getOverridableGetters() {
            return overridableGetters;
        }

        public List<Method> getOverridableSetters() {
            return overridableSetters;
        }

        public Class<?> getType() {
            if (mainGetter != null) {
                return mainGetter.getReturnType();
            }
            return setters.get(0).getParameterTypes()[0];
        }

        public Type getGenericType() {
            if (mainGetter != null) {
                return mainGetter.getGenericReturnType();
            }
            return setters.get(0).getGenericParameterTypes()[0];
        }

        public void addGetter(MethodMetadata metadata) {
            if (metadata.shouldOverride()) {
                overridableGetters.add(metadata);
            }
            getters.add(metadata);
            if (mainGetter == null) {
                mainGetter = metadata;
            } else if (!mainGetter.shouldImplement() && metadata.shouldImplement()) {
                // Prefer a real method over synthetic
                mainGetter = metadata;
            } else if (mainGetter.getReturnType().equals(Boolean.TYPE) && !metadata.getReturnType().equals(Boolean.TYPE)) {
                // Prefer non-boolean over boolean
                mainGetter = metadata;
            } else if (mainGetter.getReturnType().isAssignableFrom(metadata.getReturnType())) {
                // Prefer the most specialized type
                mainGetter = metadata;
            }
        }

        public void addSetter(Method method) {
            if (method.isBridge()) {
                // Ignore bridge methods and use the real method instead
                return;
            }
            setters.add(method);
            if (!Modifier.isFinal(method.getModifiers())) {
                overridableSetters.add(method);
            }
        }

        public void addSetMethod(Method method) {
            setMethods.add(method);
        }

        public void field(Field backingField) {
            this.backingField = backingField;
        }
    }

    private interface ClassValidator {
        /**
         * Validates the method is declared properly.
         *
         * Implementations might check things like the annotations declared on the method.
         *
         * @param method the method to check
         * @param accessorType the type of property this method would represent
         */
        default void validateMethod(Method method, PropertyAccessorType accessorType) {

        }

        /**
         * Validate the property is declared properly.
         *
         * Implementations might check things like the name or type of property.
         *
         * @param property the property to check
         */
        default void validateProperty(PropertyDetails property) {

        }
    }

    private static class ClassGenerationHandler {
        // used in subclasses
        void startType(Class<?> type) {
        }

        /**
         * Collect information about an instance method. This is called for all instance methods that are not property getter or setter methods.
         */
        void visitInstanceMethod(Method method) {
        }

        /**
         * Collect information about a property. This is called for all properties of a type.
         */
        void visitProperty(PropertyMetadata property) {
        }

        /**
         * Called when the type has any non-static fields.
         */
        public void hasFields() {
        }

        /**
         * Handler can claim the property, taking responsibility for generating whatever is required to make the property work.
         * Handler is also expected to take care of validation.
         */
        boolean claimPropertyImplementation(PropertyMetadata property) {
            return false;
        }

        /**
         * Called when another a handler with higher precedence has also claimed the given property.
         */
        void ambiguous(PropertyMetadata property) {
            // No supposed to happen
            throw new UnsupportedOperationException("Multiple matches for " + property.getName());
        }

        void applyTo(ClassInspectionVisitor visitor) {
        }

        void applyTo(ClassGenerationVisitor visitor) {
        }
    }

    private interface UnclaimedPropertyHandler {
        /**
         * Called when no handler has claimed the property.
         */
        void unclaimed(PropertyMetadata property);
    }

    private static class DslMixInPropertyType extends ClassGenerationHandler {
        private final AbstractClassGenerator.ExtensibleTypePropertyHandler extensibleTypeHandler;
        private boolean providesOwnDynamicObject;
        private boolean needDynamicAware;
        private boolean needGroovyObject;
        private boolean providesOwnToString;
        private final List<PropertyMetadata> mutableProperties = new ArrayList<>();
        private final MethodSet actionMethods = new MethodSet();
        private final SetMultimap<String, Method> closureMethods = LinkedHashMultimap.create();

        public DslMixInPropertyType(ExtensibleTypePropertyHandler extensibleTypeHandler) {
            this.extensibleTypeHandler = extensibleTypeHandler;
        }

        @Override
        void startType(Class<?> type) {
            needDynamicAware = !DynamicObjectAware.class.isAssignableFrom(type);
            needGroovyObject = !GroovyObject.class.isAssignableFrom(type);
        }

        @Override
        void visitProperty(PropertyMetadata property) {
            if (!property.isWritable()) {
                return;
            }
            if (Iterable.class.isAssignableFrom(property.getType())) {
                // Currently not supported
                return;
            }
            mutableProperties.add(property);
        }

        @Override
        boolean claimPropertyImplementation(PropertyMetadata property) {
            if (property.getName().equals("asDynamicObject")) {
                providesOwnDynamicObject = true;
                return true;
            }
            return false;
        }

        @Override
        public void visitInstanceMethod(Method method) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 0) {
                if (method.getName().equals("toString") && method.getDeclaringClass() != Object.class) {
                    providesOwnToString = true;
                }
            } else {
                Class<?> lastParameterType = parameterTypes[parameterTypes.length - 1];
                if (lastParameterType.equals(Action.class) || lastParameterType.equals(IsolatedAction.class)) {
                    actionMethods.add(method);
                } else if (lastParameterType.equals(Closure.class)) {
                    closureMethods.put(method.getName(), method);
                }
            }
        }

        @Override
        void applyTo(ClassInspectionVisitor visitor) {
            if (providesOwnDynamicObject) {
                visitor.providesOwnDynamicObjectImplementation();
            }
            if (providesOwnToString) {
                visitor.providesOwnToString();
            }
        }

        @Override
        void applyTo(ClassGenerationVisitor visitor) {
            if (needDynamicAware) {
                visitor.mixInDynamicAware();
            }
            if (needGroovyObject) {
                visitor.mixInGroovyObject();
            }
            visitor.addDynamicMethods();
            addMissingClosureOverloads(visitor);
            addSetMethods(visitor);
        }

        private void addSetMethods(AbstractClassGenerator.ClassGenerationVisitor visitor) {
            for (PropertyMetadata property : mutableProperties) {
                if (property.setMethods.isEmpty()) {
                    // TODO: remove in Gradle 10.0
                    Set<Class<?>> appliedTo = new HashSet<>();
                    for (Method setter : property.setters) {
                        if (appliedTo.add(setter.getParameterTypes()[0])) {
                            visitor.addSetMethod(property, setter);
                        }
                    }
                } else if (extensibleTypeHandler.conventionProperties.contains(property)) {
                    for (Method setMethod : property.setMethods) {
                        visitor.applyConventionMappingToSetMethod(property, setMethod);
                    }
                }
            }
        }

        private void addMissingClosureOverloads(ClassGenerationVisitor visitor) {
            for (Method method : actionMethods) {
                Method overload = findClosureOverload(method, closureMethods.get(method.getName()));
                if (overload == null) {
                    visitor.addActionMethod(method);
                }
            }
        }

        @Nullable
        private Method findClosureOverload(Method method, Collection<Method> candidates) {
            Class<?>[] methodParameterTypes = method.getParameterTypes();
            for (Method candidate : candidates) {
                Class<?>[] candidateParameterTypes = candidate.getParameterTypes();
                if (candidateParameterTypes.length != methodParameterTypes.length) {
                    continue;
                }
                boolean matches = true;
                for (int i = 0; i < candidateParameterTypes.length - 1; i++) {
                    if (!candidateParameterTypes[i].equals(methodParameterTypes[i])) {
                        matches = false;
                        break;
                    }
                }
                if (matches) {
                    return candidate;
                }
            }
            return null;
        }
    }

    private class ExtensibleTypePropertyHandler extends ClassGenerationHandler implements UnclaimedPropertyHandler {
        private Class<?> type;
        private Class<?> noMappingClass;
        private boolean conventionAware;
        private boolean extensible;
        private boolean hasExtensionAwareImplementation;
        private final List<PropertyMetadata> conventionProperties = new ArrayList<>();

        @Override
        void startType(Class<?> type) {
            this.type = type;
            extensible = !JavaReflectionUtil.hasAnnotation(type, NonExtensible.class);

            noMappingClass = Object.class;
            for (Class<?> c = type; c != null && noMappingClass == Object.class; c = c.getSuperclass()) {
                if (c.getAnnotation(NoConventionMapping.class) != null) {
                    noMappingClass = c;
                }
            }

            conventionAware = extensible && noMappingClass != type;
        }

        @Override
        boolean claimPropertyImplementation(PropertyMetadata property) {
            if (extensible) {
                if (property.getName().equals("extensions")) {
                    for (MethodMetadata getter : property.getOverridableGetters()) {
                        if (getter.isAbstract()) {
                            return true;
                        }
                    }
                    hasExtensionAwareImplementation = true;
                    return true;
                }
                return property.getName().equals("conventionMapping")
                    || property.getName().equals("convention");
            }

            return false;
        }

        @Override
        public void unclaimed(PropertyMetadata property) {
            for (MethodMetadata getter : property.getOverridableGetters()) {
                if (!getter.method.getDeclaringClass().isAssignableFrom(noMappingClass)) {
                    conventionProperties.add(property);
                    break;
                }
            }
        }

        @Override
        void applyTo(ClassInspectionVisitor visitor) {
            if (extensible) {
                visitor.mixInExtensible();
            }
            if (conventionAware) {
                visitor.mixInConventionAware();
            }
            for (PropertyMetadata property : conventionProperties) {
                boolean applyRole = isLazyAttachProperty(property) && isRoleType(property);
                if (applyRole) {
                    visitor.instantiatesNestedObjects();
                }
            }
        }

        @Override
        void applyTo(ClassGenerationVisitor visitor) {
            boolean addExtensionProperty = extensible && !hasExtensionAwareImplementation;
            boolean mixInConventionAware = conventionAware && !IConventionAware.class.isAssignableFrom(type);
            if (addExtensionProperty || mixInConventionAware) {
                visitor.addNoDeprecationConventionPrivateGetter();
            }
            if (addExtensionProperty) {
                visitor.addExtensionsProperty();
            }
            if (mixInConventionAware) {
                visitor.mixInConventionAware();
            }
            for (PropertyMetadata property : conventionProperties) {
                visitor.applyConventionMappingToProperty(property);
                boolean attachProperty = isLazyAttachProperty(property);
                boolean applyRole = attachProperty && isRoleType(property);
                for (MethodMetadata getter : property.getOverridableGetters()) {
                    boolean attachOwner = attachProperty && isAttachableType(getter);
                    visitor.applyConventionMappingToGetter(property, getter, attachOwner, applyRole);
                }
                for (Method setter : property.getOverridableSetters()) {
                    visitor.applyConventionMappingToSetter(property, setter);
                }
            }
        }
    }

    private class ManagedPropertiesHandler extends ClassGenerationHandler {
        private final List<PropertyMetadata> mutableProperties = new ArrayList<>();
        private final List<PropertyMetadata> readOnlyProperties = new ArrayList<>();
        private final List<PropertyMetadata> eagerAttachProperties = new ArrayList<>();
        private final List<PropertyMetadata> ineligibleProperties = new ArrayList<>();

        private boolean hasFields;

        @Override
        public void hasFields() {
            hasFields = true;
        }

        @Override
        void visitProperty(PropertyMetadata property) {
            if (isEagerAttachProperty(property)) {
                // Property is read-only and main getter is final, so attach eagerly in constructor
                // If the getter is not final, then attach lazily in the getter
                eagerAttachProperties.add(property);
            }

            if (isIneligibleForConventionMapping(property)) {
                ineligibleProperties.add(property);
            }
        }

        @Override
        boolean claimPropertyImplementation(PropertyMetadata property) {
            // Skip properties with non-abstract getter or setter implementations
            for (MethodMetadata getter : property.getters) {
                if (getter.shouldImplement() && !getter.isAbstract()) {
                    return false;
                }
            }
            for (Method setter : property.setters) {
                if (!Modifier.isAbstract(setter.getModifiers())) {
                    return false;
                }
            }

            // Property is readable and all getters and setters are abstract
            if (isManagedProperty(property)) {
                // Abstract read-only property with managed type
                readOnlyProperties.add(property);
                return true;
            } else if (property.isReadable() && property.isWritable()) {
                // Mutable property
                mutableProperties.add(property);
                return true;
            } else {
                // Read only but unrecognized type
                return false;
            }
        }

        @Override
        void applyTo(ClassInspectionVisitor visitor) {
            if (!hasFields) {
                visitor.mixInFullyManagedState();
            }
            if (!readOnlyProperties.isEmpty()) {
                visitor.instantiatesNestedObjects();
            }
            for (PropertyMetadata property : eagerAttachProperties) {
                boolean applyRole = isRoleType(property);
                visitor.attachDuringConstruction(property, applyRole);
            }
            for (PropertyMetadata property : ineligibleProperties) {
                visitor.markPropertyAsIneligibleForConventionMapping(property);
            }
            for (PropertyMetadata property : readOnlyProperties) {
                if (isReattachProperty(property)) {
                    boolean applyRole = isRoleType(property);
                    visitor.attachOnDemand(property, applyRole);
                }
            }
        }

        @Override
        void applyTo(ClassGenerationVisitor visitor) {
            for (PropertyMetadata property : mutableProperties) {
                visitor.applyManagedStateToProperty(property);
                for (MethodMetadata getter : property.getters) {
                    visitor.applyManagedStateToGetter(property, getter.method);
                }
                for (Method setter : property.setters) {
                    visitor.applyManagedStateToSetter(property, setter);
                }
            }
            for (PropertyMetadata property : readOnlyProperties) {
                visitor.applyManagedStateToProperty(property);
                boolean applyRole = isRoleType(property);
                for (MethodMetadata getter : property.getters) {
                    visitor.applyReadOnlyManagedStateToGetter(property, getter.method, applyRole);
                }
            }
            if (!hasFields) {
                visitor.addManagedMethods(mutableProperties, readOnlyProperties);
            }
        }
    }

    private static class NamePropertyHandler extends ClassGenerationHandler {

        private PropertyMetadata nameProperty;

        @Override
        void visitProperty(PropertyMetadata property) {
            if (isNameProperty(property)) {
                nameProperty = property;
            }
        }

        @Override
        boolean claimPropertyImplementation(PropertyMetadata property) {
            if (isNameProperty(property)) {
                nameProperty = property;
                return true;
            }
            return false;
        }

        @Override
        void applyTo(ClassGenerationVisitor visitor) {
            if (nameProperty != null) {
                visitor.addNameProperty();
            }
        }

        boolean hasNameProperty() {
            return nameProperty != null;
        }
    }

    private static class LazyGroovySupportTypePropertyHandler extends ClassGenerationHandler {
        private final List<PropertyMetadata> lazyGroovySupportTyped = new ArrayList<>();

        @Override
        void visitProperty(PropertyMetadata property) {
            // For ConfigurableFileCollection we generate setters just for readonly properties,
            // since we want to support += for mutable FileCollection properties, but we don't support += for ConfigurableFileCollection (yet).
            // And if we generate setter override for ConfigurableFileCollection, it's difficult to distinguish between these two cases in setFromAnyValue method.
            if ((property.isReadable() && hasPropertyType(property)) ||
                (property.isReadOnly() && isConfigurableFileCollectionType(property.getType()))) {
                lazyGroovySupportTyped.add(property);
            }
        }

        @Override
        void applyTo(ClassGenerationVisitor visitor) {
            for (PropertyMetadata property : lazyGroovySupportTyped) {
                visitor.addLazyGroovySupportSetterOverloads(property, property.mainGetter);
            }
        }
    }

    private static class InjectionAnnotationValidator implements ClassValidator {
        private final Set<Class<? extends Annotation>> annotationTypes;
        private final ImmutableMultimap<Class<? extends Annotation>, TypeToken<?>> allowedTypesForAnnotation;

        InjectionAnnotationValidator(Set<Class<? extends Annotation>> annotationTypes, ImmutableMultimap<Class<? extends Annotation>, TypeToken<?>> allowedTypesForAnnotation) {
            this.annotationTypes = annotationTypes;
            this.allowedTypesForAnnotation = allowedTypesForAnnotation;
        }

        @Override
        public void validateMethod(Method method, PropertyAccessorType accessorType) {
            List<Class<? extends Annotation>> matches = new ArrayList<>();
            validateMethod(method, accessorType, Inject.class, matches);
            for (Class<? extends Annotation> annotationType : annotationTypes) {
                validateMethod(method, accessorType, annotationType, matches);
            }
            if (matches.size() > 1) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(matches.get(0));
                formatter.append(" and ");
                formatter.appendAnnotation(matches.get(1));
                formatter.append(" annotations together on method ");
                formatter.appendMethod(method);
                formatter.append(".");
                throw new IllegalArgumentException(formatter.toString());
            }
        }

        private void validateMethod(Method method, PropertyAccessorType accessorType, Class<? extends Annotation> annotationType, List<Class<? extends Annotation>> matches) {
            if (method.getAnnotation(annotationType) == null) {
                return;
            }
            matches.add(annotationType);
            if (Modifier.isStatic(method.getModifiers())) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(annotationType);
                formatter.append(" annotation on method ");
                formatter.appendMethod(method);
                formatter.append(" as it is static.");
                throw new IllegalArgumentException(formatter.toString());
            }
            if (accessorType != PropertyAccessorType.GET_GETTER) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(annotationType);
                formatter.append(" annotation on method ");
                formatter.appendMethod(method);
                formatter.append(" as it is not a property getter.");
                throw new IllegalArgumentException(formatter.toString());
            }
            if (Modifier.isFinal(method.getModifiers())) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(annotationType);
                formatter.append(" annotation on method ");
                formatter.appendMethod(method);
                formatter.append(" as it is final.");
                throw new IllegalArgumentException(formatter.toString());
            }
            if (!Modifier.isPublic(method.getModifiers()) && !Modifier.isProtected(method.getModifiers())) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(annotationType);
                formatter.append(" annotation on method ");
                formatter.appendMethod(method);
                formatter.append(" as it is not public or protected.");
                throw new IllegalArgumentException(formatter.toString());
            }
            ImmutableCollection<TypeToken<?>> allowedTypes = allowedTypesForAnnotation.get(annotationType);
            if (!allowedTypes.isEmpty()) {
                Type returnType = method.getGenericReturnType();
                for (TypeToken<?> allowedType : allowedTypes) {
                    if (allowedType.isSubtypeOf(returnType)) {
                        return;
                    }
                }
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(annotationType);
                formatter.append(" annotation on property '");
                formatter.append(accessorType.propertyNameFor(method));
                formatter.append("' of type ");
                formatter.append(TypeToken.of(returnType).toString());
                formatter.append(". Allowed property types: ");
                formatter.append(allowedTypes.stream()
                    .map(TypeToken::toString)
                    .sorted()
                    .collect(Collectors.joining(", "))
                );
                formatter.append(".");
                throw new IllegalArgumentException(formatter.toString());
            }
        }
    }

    private static class ServicesPropertyHandler extends ClassGenerationHandler {
        private boolean hasServicesProperty;

        @Override
        public boolean claimPropertyImplementation(PropertyMetadata property) {
            if (property.getName().equals("services") && property.isReadable() && ServiceRegistry.class.isAssignableFrom(property.getType())) {
                hasServicesProperty = true;
                return true;
            }
            return false;
        }

        @Override
        void applyTo(ClassInspectionVisitor visitor) {
            if (hasServicesProperty) {
                visitor.providesOwnServicesImplementation();
            }
        }
    }

    private static abstract class AbstractInjectedPropertyHandler extends ClassGenerationHandler {
        final Class<? extends Annotation> annotation;
        final List<PropertyMetadata> serviceInjectionProperties = new ArrayList<>();

        public AbstractInjectedPropertyHandler(Class<? extends Annotation> annotation) {
            this.annotation = annotation;
        }

        @Override
        public boolean claimPropertyImplementation(PropertyMetadata property) {
            for (MethodMetadata getter : property.getters) {
                if (getter.method.getAnnotation(annotation) != null) {
                    serviceInjectionProperties.add(property);
                    return true;
                }
            }
            return false;
        }

        @Override
        void ambiguous(PropertyMetadata property) {
            for (MethodMetadata getter : property.getters) {
                if (getter.method.getAnnotation(annotation) != null) {
                    TreeFormatter formatter = new TreeFormatter();
                    formatter.node("Cannot use ");
                    formatter.appendAnnotation(annotation);
                    formatter.append(" annotation on method ");
                    formatter.appendMethod(getter.method);
                    formatter.append(".");
                    throw new IllegalArgumentException(formatter.toString());
                }
            }
            super.ambiguous(property);
        }

        @Override
        void applyTo(ClassInspectionVisitor visitor) {
            if (!serviceInjectionProperties.isEmpty()) {
                visitor.mixInServiceInjection();
            }
        }

        public List<Class<?>> getInjectedServices() {
            ImmutableList.Builder<Class<?>> services = ImmutableList.builderWithExpectedSize(serviceInjectionProperties.size());
            for (PropertyMetadata property : serviceInjectionProperties) {
                services.add(property.getType());
            }
            return services.build();
        }

        public boolean isUsed() {
            return !serviceInjectionProperties.isEmpty();
        }

        public Class<? extends Annotation> getAnnotation() {
            return annotation;
        }
    }

    private static class InjectAnnotationPropertyHandler extends AbstractInjectedPropertyHandler {
        public InjectAnnotationPropertyHandler() {
            super(Inject.class);
        }

        @Override
        public void applyTo(ClassGenerationVisitor visitor) {
            for (PropertyMetadata property : serviceInjectionProperties) {
                visitor.applyServiceInjectionToProperty(property);
                for (MethodMetadata getter : property.getOverridableGetters()) {
                    visitor.applyServiceInjectionToGetter(property, getter);
                }
                for (Method setter : property.getOverridableSetters()) {
                    visitor.applyServiceInjectionToSetter(property, setter);
                }
            }
        }
    }

    private static class CustomInjectAnnotationPropertyHandler extends AbstractInjectedPropertyHandler {
        public CustomInjectAnnotationPropertyHandler(Class<? extends Annotation> injectAnnotation) {
            super(injectAnnotation);
        }

        @Override
        public void applyTo(ClassGenerationVisitor visitor) {
            for (PropertyMetadata property : serviceInjectionProperties) {
                visitor.applyServiceInjectionToProperty(property);
                for (MethodMetadata getter : property.getOverridableGetters()) {
                    visitor.applyServiceInjectionToGetter(property, annotation, getter);
                }
                for (Method setter : property.getOverridableSetters()) {
                    visitor.applyServiceInjectionToSetter(property, annotation, setter);
                }
            }
        }
    }

    private static class DisabledAnnotationValidator implements ClassValidator {
        private final Class<? extends Annotation> annotation;

        public DisabledAnnotationValidator(Class<? extends Annotation> annotation) {
            this.annotation = annotation;
        }

        @Override
        public void validateMethod(Method method, PropertyAccessorType accessorType) {
            if (method.getAnnotation(annotation) != null) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Cannot use ");
                formatter.appendAnnotation(annotation);
                formatter.append(" annotation on method ");
                formatter.appendMethod(method);
                formatter.append(".");

                throw new IllegalArgumentException(formatter.toString());
            }
        }
    }

    protected interface ClassInspectionVisitor {
        void mixInExtensible();

        void mixInConventionAware();

        void providesOwnDynamicObjectImplementation();

        void providesOwnServicesImplementation();

        void providesOwnToString();

        void mixInFullyManagedState();

        void mixInServiceInjection();

        void instantiatesNestedObjects();

        void attachDuringConstruction(PropertyMetadata property, boolean applyRole);

        void attachOnDemand(PropertyMetadata property, boolean applyRole);

        void markPropertyAsIneligibleForConventionMapping(PropertyMetadata property);

        ClassGenerationVisitor builder();
    }

    protected interface InstantiationStrategy {
        Object newInstance(ServiceLookup services, InstanceGenerator nested, @Nullable Describable displayName, Object[] params) throws InvocationTargetException, IllegalAccessException, InstantiationException;
    }

    protected interface ClassGenerationVisitor {
        void addConstructor(Constructor<?> constructor, boolean addNameParameter);

        default void addConstructor(Constructor<?> constructor) {
            addConstructor(constructor, false);
        }

        void addDefaultConstructor();

        void addNameConstructor();

        void mixInDynamicAware();

        void addNoDeprecationConventionPrivateGetter();

        void mixInConventionAware();

        void mixInGroovyObject();

        void addDynamicMethods();

        void addExtensionsProperty();

        void applyServiceInjectionToProperty(PropertyMetadata property);

        void applyServiceInjectionToGetter(PropertyMetadata property, MethodMetadata getter);

        void applyServiceInjectionToSetter(PropertyMetadata property, Method setter);

        void applyServiceInjectionToGetter(PropertyMetadata property, Class<? extends Annotation> annotation, MethodMetadata getter);

        void applyServiceInjectionToSetter(PropertyMetadata property, Class<? extends Annotation> annotation, Method setter);

        void applyManagedStateToProperty(PropertyMetadata property);

        void applyManagedStateToGetter(PropertyMetadata property, Method getter);

        void applyManagedStateToSetter(PropertyMetadata property, Method setter);

        void applyReadOnlyManagedStateToGetter(PropertyMetadata property, Method getter, boolean applyRole);

        void addManagedMethods(List<PropertyMetadata> mutableProperties, List<PropertyMetadata> readOnlyProperties);

        void applyConventionMappingToProperty(PropertyMetadata property);

        void applyConventionMappingToGetter(PropertyMetadata property, MethodMetadata getter, boolean attachOwner, boolean applyRole);

        void applyConventionMappingToSetter(PropertyMetadata property, Method setter);

        void applyConventionMappingToSetMethod(PropertyMetadata property, Method setter);

        void addSetMethod(PropertyMetadata propertyMetaData, Method setter);

        void addActionMethod(Method method);

        void addLazyGroovySupportSetterOverloads(PropertyMetadata property, MethodMetadata getter);

        void addNameProperty();

        Class<?> generate() throws Exception;
    }

    /**
     * Groovy 4 no longer considers {@code Boolean isFoo()} a property {@code foo} of type {@code Boolean}.
     * <p>
     * To create a boolean property, you need to use {@code boolean isFoo()} or {@code Boolean getFoo()}.
     */
    @NonNullApi
    private static class BooleanPropertyDeprecatingValidator implements ClassValidator {
        @Override
        public void validateProperty(PropertyDetails property) {
            // If a property only has a single getter, we need to check if its getXXX or isXXX when it's a Boolean.
            // In a future version of Groovy, the Groovy compiler will not consider a Boolean is-getter as defining a property.
            //
            // However, in Groovy 3, the compiler will still generate getters for a field-like declaration:
            // class GroovyClass {
            //      Boolean foo
            // }
            // This will generate a private field "foo", a getFoo() and a isFoo().
            // We do not emit a deprecation if a property has both a get-getter and an is-getter.
            //
            // In Groovy 4, the same code will only generate the field and getFoo().
            //
            if (property.getGetters().size() == 1) {
                Method method = property.getGetters().iterator().next();
                PropertyAccessorType accessorType = PropertyAccessorType.of(method);
                if (accessorType == PropertyAccessorType.IS_GETTER && method.getReturnType().isAssignableFrom(Boolean.class)) {
                    // To remove this deprecation, we need to do a few things:
                    // 1. We should no longer recognize isXXX for anything that is not explicitly boolean (primitive type)
                    // 2. We should be able to remove this validator completely. We do not need to make this an error.
                    //
                    // If we do not upgrade to Groovy 4 in Gradle 9, we can still remove this deprecation and drop support for these types of properties.

                    DeprecationLogger.deprecateAction("Declaring an 'is-' property with a Boolean type")
                        .withAdvice(String.format(
                            "Add a method named '%s' with the same behavior and mark the old one with @Deprecated, or change the type of '%s.%s' (and the setter) to 'boolean'.",
                            method.getName().replace("is", "get"),
                            method.getDeclaringClass().getCanonicalName(), method.getName()
                        ))
                        .withContext("The combination of method name and return type is not consistent with Java Bean property rules and will become unsupported in future versions of Groovy.")
                        .startingWithGradle9("this property will be ignored by Gradle")
                        .withUpgradeGuideSection(8, "groovy_boolean_properties")
                        .nagUser();
                }
            }
        }
    }
}
