/*
 * 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 groovy.transform.Generated;
import org.gradle.api.Action;
import org.gradle.api.Describable;
import org.gradle.api.IsolatedAction;
import org.gradle.api.NonExtensible;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.internal.DynamicObjectAware;
import org.gradle.api.internal.IConventionAware;
import org.gradle.api.internal.plugins.software.SoftwareType;
import org.gradle.api.model.ManagedType;
import org.gradle.api.plugins.ExtensionAware;
import org.gradle.api.provider.HasMultipleValues;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
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 org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.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.function.Predicate;
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 {

    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() && (property.getType().isAnnotationPresent(ManagedType.class) || hasNestedAnnotation(property::hasAnnotation));
    }

    private static boolean hasNestedAnnotation(Predicate<Class<? extends Annotation>> hasAnnotation) {
        return NESTED_ANNOTATION_TYPES.stream().anyMatch(hasAnnotation);
    }

    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());
    }

    /**
     * Determine if, should the property need to be attached, if it should be done lazily in the main getter, or eagerly in the constructor.
     *
     * @param property the property to check
     * @return {@code true} if the property should be lazily attached, {@code false} if it should be eagerly attached
     */
    private static boolean isLazyAttachPropertyIfNeeded(PropertyMetadata property) {
        if (property.getMainGetter().isAbstract()) {
            // All abstract properties can be lazily attached
            return true;
        }
        if (!property.getOverridableGetters().isEmpty()) {
            // All properties that override a method can be lazily attached,
            // unless they have a known backing field and @Generated, in which case we need to eagerly attach them
            // to keep Groovy properties working inside their own class.

            // In theory, we should eagerly attach all overridable properties just in case,
            // but that would break existing code that relies on lazy attachment of properties.
            return property.getBackingField() == null || !property.getMainGetter().method.isAnnotationPresent(Generated.class);
        }
        // Other Property should be eagerly attached, as they are not overridable.
        // Other non-Property properties cannot be eagerly attached for backwards compatibility reasons.
        return !hasPropertyType(property);
    }

    private static boolean isAttachProperty(PropertyMetadata property) {
        return property.isReadableWithoutSetterOfPropertyType() && isAttachableType(property.getType(), property::hasAnnotation);
    }

    private static boolean isAttachableMethod(MethodMetadata metadata) {
        return isAttachableType(metadata.getReturnType(), metadata.method::isAnnotationPresent);
    }

    private static boolean isAttachableType(Class<?> type, Predicate<Class<? extends Annotation>> propertyHasAnnotation) {
        // This should apply to all 'managed' types however only the ConfigurableFileCollection and Provider types and @Nested value current implement OwnerAware
        return Provider.class.isAssignableFrom(type) || isConfigurableFileCollectionType(type) || hasNestedAnnotation(propertyHasAnnotation);
    }

    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 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;
        }

        @Nullable
        public Field getBackingField() {
            return backingField;
        }

        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
                    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 = isAttachProperty(property) && isLazyAttachPropertyIfNeeded(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) {
                visitor.addExtensionsProperty();
            }
            if (mixInConventionAware) {
                visitor.mixInConventionAware();
            }
            for (PropertyMetadata property : conventionProperties) {
                visitor.applyConventionMappingToProperty(property);
                boolean attachProperty = isAttachProperty(property) && isLazyAttachPropertyIfNeeded(property);
                boolean applyRole = attachProperty && isRoleType(property);
                for (MethodMetadata getter : property.getOverridableGetters()) {
                    boolean attachOwner = attachProperty && isAttachableMethod(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 (isAttachProperty(property) && !isLazyAttachPropertyIfNeeded(property)) {
                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 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()}.
     */
    @NullMarked
    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.
                    // See PropertyAccessorType, BeanDynamicObject and DefaultTypeAnnotationMetadataStore for similar special handling
                    DeprecationLogger.deprecateAction("Declaring '" + property.getName() + "' 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, 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();
                }
            }
        }
    }
}
