/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.runtime.metaclass;

import groovy.lang.ExpandoMetaClass;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MetaClass;
import groovy.lang.MetaClassRegistry;
import groovy.lang.MetaClassRegistryChangeEvent;
import groovy.lang.MetaClassRegistryChangeEventListener;
import groovy.lang.MetaMethod;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.reflection.CachedClass;
import org.codehaus.groovy.reflection.CachedMethod;
import org.codehaus.groovy.reflection.ClassInfo;
import org.codehaus.groovy.reflection.GeneratedMetaMethod;
import org.codehaus.groovy.reflection.ReflectionCache;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.DefaultGroovyStaticMethods;
import org.codehaus.groovy.runtime.m12n.ExtensionModule;
import org.codehaus.groovy.runtime.m12n.ExtensionModuleRegistry;
import org.codehaus.groovy.runtime.m12n.ExtensionModuleScanner;
import org.codehaus.groovy.util.FastArray;
import org.codehaus.groovy.util.ManagedConcurrentLinkedQueue;
import org.codehaus.groovy.util.ReferenceBundle;
import org.codehaus.groovy.vmplugin.VMPluginFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A registry of MetaClass instances which caches introspection and
 * reflection information and allows methods to be dynamically added to
 * existing classes at runtime
 */
public class MetaClassRegistryImpl implements MetaClassRegistry {
    /**
     * @deprecated Use {@link ExtensionModuleScanner#MODULE_META_INF_FILE} instead
     */
    @Deprecated
    public static final String MODULE_META_INF_FILE = "META-INF/services/org.codehaus.groovy.runtime.ExtensionModule";
    private static final MetaClass[] EMPTY_METACLASS_ARRAY = new MetaClass[0];
    private static final MetaClassRegistryChangeEventListener[] EMPTY_METACLASSREGISTRYCHANGEEVENTLISTENER_ARRAY = new MetaClassRegistryChangeEventListener[0];

    /**
     * System property name used to disable specific extension modules.
     * When set, the value should be a comma-separated list of extension module names to disable.
     */
    public static final String EXTENSION_DISABLE_PROPERTY = "groovy.extension.disable";

    private final boolean useAccessible;

    private final FastArray instanceMethods = new FastArray();
    private final FastArray staticMethods = new FastArray();

    private final LinkedList<MetaClassRegistryChangeEventListener> changeListenerList = new LinkedList<MetaClassRegistryChangeEventListener>();
    private final LinkedList<MetaClassRegistryChangeEventListener> nonRemoveableChangeListenerList = new LinkedList<MetaClassRegistryChangeEventListener>();
    private final ManagedConcurrentLinkedQueue<MetaClass> metaClassInfo = new ManagedConcurrentLinkedQueue<MetaClass>(ReferenceBundle.getWeakBundle());
    private final ExtensionModuleRegistry moduleRegistry = new ExtensionModuleRegistry();
    private final String disabledString = SystemUtil.getSystemPropertySafe(EXTENSION_DISABLE_PROPERTY);
    private final boolean disabling = disabledString != null;
    private final List<DisabledMethodSpec> disabledSpecs = disabling ? DisabledMethodSpec.parse(disabledString) : null;

    /**
     * Flag to load default metaclasses for standard Java and Groovy types when initializing the registry.
     */
    public static final int LOAD_DEFAULT = 0;

    /**
     * Flag to skip loading default metaclasses during registry initialization.
     */
    public static final int DONT_LOAD_DEFAULT = 1;
    private static MetaClassRegistry instanceInclude;
    private static MetaClassRegistry instanceExclude;

    /**
     * Creates a new MetaClassRegistry with default settings.
     * Loads default metaclasses for standard Java and Groovy types.
     */
    public MetaClassRegistryImpl() {
        this(LOAD_DEFAULT, true);
    }

    /**
     * Creates a new MetaClassRegistry with optional default metaclass loading.
     *
     * @param loadDefault either LOAD_DEFAULT to load default metaclasses or DONT_LOAD_DEFAULT to skip loading
     */
    public MetaClassRegistryImpl(int loadDefault) {
        this(loadDefault, true);
    }

    /**
     * @param useAccessible defines whether the {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)}
     *                      method will be called to enable access to all methods when using reflection
     */
    public MetaClassRegistryImpl(boolean useAccessible) {
        this(LOAD_DEFAULT, useAccessible);
    }

    /**
     * Creates a new MetaClassRegistry with customizable settings.
     *
     * @param loadDefault either LOAD_DEFAULT to load default metaclasses or DONT_LOAD_DEFAULT to skip loading
     * @param useAccessible whether to use AccessibleObject.setAccessible() for reflection access
     */
    public MetaClassRegistryImpl(final int loadDefault, final boolean useAccessible) {
        this.useAccessible = useAccessible;

        if (loadDefault == LOAD_DEFAULT) {
            final Map<CachedClass, List<MetaMethod>> map = new HashMap<CachedClass, List<MetaMethod>>();

            // let's register the default methods
            registerMethods(null, true, true, map);
            final Class[] additionals = DefaultGroovyMethods.ADDITIONAL_CLASSES;
            for (int i = 0; i != additionals.length; ++i) {
                createMetaMethodFromClass(map, additionals[i]);
            }

            Class[] pluginDGMs = VMPluginFactory.getPlugin().getPluginDefaultGroovyMethods();
            for (Class plugin : pluginDGMs) {
                registerMethods(plugin, false, true, map);
            }
            registerMethods(DefaultGroovyStaticMethods.class, false, false, map);
            Class[] staticPluginDGMs = VMPluginFactory.getPlugin().getPluginStaticGroovyMethods();
            for (Class plugin : staticPluginDGMs) {
                registerMethods(plugin, false, false, map);
            }

            ExtensionModuleScanner scanner = new ExtensionModuleScanner(new DefaultModuleListener(map), this.getClass().getClassLoader());
            scanner.scanClasspathModules();

            checkForDuplicateConcurrentModule();

            refreshMopMethods(map);

        }

        installMetaClassCreationHandle();

        final MetaClass emcMetaClass = metaClassCreationHandle.create(ExpandoMetaClass.class, this);
        emcMetaClass.initialize();
        ClassInfo.getClassInfo(ExpandoMetaClass.class).setStrongMetaClass(emcMetaClass);


        addNonRemovableMetaClassRegistryChangeEventListener(cmcu -> {
            // The calls to DefaultMetaClassInfo.setPrimitiveMeta and sdyn.setBoolean need to be
            // ordered. Even though metaClassInfo is thread-safe, it is included in the block
            // so the metaclasses are added to the queue in the same order.
            synchronized (metaClassInfo) {
               metaClassInfo.add(cmcu.getNewMetaClass());
               DefaultMetaClassInfo.getNewConstantMetaClassVersioning();
               Class c = cmcu.getClassToUpdate();
               DefaultMetaClassInfo.setPrimitiveMeta(c, cmcu.getNewMetaClass()==null);
               Field sdyn;
               try {
                   sdyn = c.getDeclaredField(Verifier.STATIC_METACLASS_BOOL);
                   sdyn.setBoolean(null, cmcu.getNewMetaClass()!=null);
               } catch (Throwable e) {
                   //DO NOTHING
               }
            }
        });
   }

    private static void refreshMopMethods(final Map<CachedClass, List<MetaMethod>> map) {
        for (Map.Entry<CachedClass, List<MetaMethod>> e : map.entrySet()) {
            CachedClass cls = e.getKey();
            cls.setNewMopMethods(e.getValue());
        }
    }

    /**
     * Registers extension module methods from the given properties.
     * This method scans for extension modules defined in properties and registers their methods.
     *
     * @param properties the properties containing extension module definitions
     * @param classLoader the class loader to use for loading extension modules
     * @param map the map to store the loaded meta methods
     */
    public void registerExtensionModuleFromProperties(final Properties properties, final ClassLoader classLoader, final Map<CachedClass, List<MetaMethod>> map) {
        ExtensionModuleScanner scanner = new ExtensionModuleScanner(new DefaultModuleListener(map), classLoader);
        scanner.scanExtensionModuleFromProperties(properties);
    }

    /**
     * Gets the extension module registry.
     * The registry keeps track of all loaded extension modules.
     *
     * @return the extension module registry
     */
    public ExtensionModuleRegistry getModuleRegistry() {
        return moduleRegistry;
    }

    /**
     * Looks for a class called 'groovy.runtime.metaclass.CustomMetaClassCreationHandle' and if it exists uses it as the MetaClassCreationHandle
     * otherwise uses the default
     *
     * @see groovy.lang.MetaClassRegistry.MetaClassCreationHandle
     */
    /**
     * Detects when the concurrent API classes appear in more than one
     * classpath location — typically when both Groovy core and the
     * standalone groovy-concurrent-java jar are present. Logs a warning
     * so Maven users (who lack Gradle's capability mechanism) are alerted.
     */
    private static void checkForDuplicateConcurrentModule() {
        try {
            ClassLoader cl = MetaClassRegistryImpl.class.getClassLoader();
            if (cl == null) return;
            var locations = java.util.Collections.list(cl.getResources("groovy/concurrent/AsyncScope.class"));
            // Only count packaged (jar) locations — a classes/ directory on the
            // classpath alongside a jar is normal during the Groovy bootstrap
            // build and not a deployment problem users can fix.
            var jarLocations = locations.stream()
                    .filter(u -> "jar".equals(u.getProtocol()))
                    .toList();
            if (jarLocations.size() > 1) {
                Logger.getLogger(MetaClassRegistryImpl.class.getName()).warning(
                        "groovy.concurrent.AsyncScope found in multiple classpath jars: " + jarLocations
                        + ". The concurrent API classes are duplicated. "
                        + "Remove groovy-concurrent-java when using the full Groovy runtime.");
            }
        } catch (Exception ignored) {
            // SecurityManager or other restriction — skip check
        }
    }

    private void installMetaClassCreationHandle() {
           try {
               final Class customMetaClassHandle = Class.forName("groovy.runtime.metaclass.CustomMetaClassCreationHandle");
               final Constructor customMetaClassHandleConstructor = customMetaClassHandle.getConstructor();
                 this.metaClassCreationHandle = (MetaClassCreationHandle)customMetaClassHandleConstructor.newInstance();
           } catch (final ClassNotFoundException e) {
               this.metaClassCreationHandle = new MetaClassCreationHandle();
           } catch (final Exception e) {
               throw new GroovyRuntimeException("Could not instantiate custom Metaclass creation handle: "+ e, e);
           }
    }

    private void registerMethods(final Class theClass, final boolean useMethodWrapper, final boolean useInstanceMethods, Map<CachedClass, List<MetaMethod>> map) {
        if (useMethodWrapper) {
            // Here we instantiate objects representing MetaMethods for DGM methods.
            // Calls for such meta methods done without reflection, so more effectively.

            try {
                List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();

                for (GeneratedMetaMethod.DgmMethodRecord record : records) {
                    if (disabling && isDisabled(record.methodName, record.parameters)) continue;
                    Class[] newParams = new Class[record.parameters.length - 1];
                    System.arraycopy(record.parameters, 1, newParams, 0, newParams.length);

                    MetaMethod method = new GeneratedMetaMethod.Proxy(
                            record.className,
                            record.methodName,
                            ReflectionCache.getCachedClass(record.parameters[0]),
                            record.returnType,
                            newParams
                    );
                    final CachedClass declClass = method.getDeclaringClass();
                    List<MetaMethod> arr = map.computeIfAbsent(declClass, k -> new ArrayList<MetaMethod>(4));
                    arr.add(method);
                    instanceMethods.add(method);
                }
            } catch (Throwable e) {
                Logger logger = Logger.getLogger(MetaClassRegistryImpl.class.getName());
                if (logger.isLoggable(Level.WARNING)) {
                    logger.warning(DefaultGroovyMethods.asString(e));
                }
                // we print the error, but we don't stop with an exception here
                // since it is more comfortable this way for development
            }
        } else {
            CachedMethod[] methods = ReflectionCache.getCachedClass(theClass).getMethods();
            for (CachedMethod method : methods) {
                if (method.isStatic() && method.isPublic() && method.getAnnotation(Deprecated.class) == null) {
                    if (disabling && isDisabled(method.getName(), method.getParameterTypes())) continue;
                    CachedClass[] paramTypes = method.getParameterTypes();
                    if (paramTypes.length > 0) {
                        List<MetaMethod> arr = map.computeIfAbsent(paramTypes[0], k -> new ArrayList<MetaMethod>(4));
                        if (useInstanceMethods) {
                            final NewInstanceMetaMethod metaMethod = new NewInstanceMetaMethod(method);
                            arr.add(metaMethod);
                            instanceMethods.add(metaMethod);
                        } else {
                            final NewStaticMetaMethod metaMethod = new NewStaticMetaMethod(method);
                            arr.add(metaMethod);
                            staticMethods.add(metaMethod);
                        }
                    }
                }
            }
        }
    }

    private void createMetaMethodFromClass(Map<CachedClass, List<MetaMethod>> map, Class aClass) {
        try {
            MetaMethod method = (MetaMethod) aClass.getDeclaredConstructor().newInstance();
            final CachedClass declClass = method.getDeclaringClass();
            List<MetaMethod> arr = map.computeIfAbsent(declClass, k -> new ArrayList<MetaMethod>(4));
            arr.add(method);
            instanceMethods.add(method);
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { /* ignore */
        }
    }

    /**
     * Gets the MetaClass for the given class.
     *
     * @param theClass the class to get the metaclass for
     * @return the metaclass for the class
     */
    @Override
    public final MetaClass getMetaClass(Class theClass) {
        return ClassInfo.getClassInfo(theClass).getMetaClass();
    }

    /**
     * Gets the metaclass for the given object.
     * For Class objects, returns the metaclass for that class; for other objects,
     * returns the instance-specific metaclass if set, otherwise the metaclass for the object's class.
     *
     * @param obj the object to get the metaclass for
     * @return the metaclass for the object
     */
    public MetaClass getMetaClass(Object obj) {
        Class theClass = obj instanceof Class ? (Class) obj : obj.getClass();
        return ClassInfo.getClassInfo(theClass).getMetaClass(obj);
    }

    /**
     * If oldMC is null, newMC will replace whatever MC is present.
     * Otherwise, newMC will be used only if the stored MC is the same as oldMC
     */
    private void setMetaClass(Class theClass, MetaClass oldMC, MetaClass newMC) {
        ClassInfo info = ClassInfo.getClassInfo(theClass);
        MetaClass mc;
        info.lock();
        try {
            mc = info.getStrongMetaClass();
            info.setStrongMetaClass(newMC);
        } finally {
            info.unlock();
        }
        if ((oldMC == null || oldMC != mc) && (newMC != mc)) {
            fireConstantMetaClassUpdate(null, theClass, mc, newMC);
        }
    }

    /**
     * Removes the metaclass for the given class, resetting it to null.
     * This forces a new metaclass to be created the next time one is needed.
     *
     * @param theClass the class to remove the metaclass for
     */
    @Override
    public void removeMetaClass(Class theClass) {
        setMetaClass(theClass, null, null);
    }

    /**
     * Sets the metaclass for the given class, replacing any existing metaclass.
     *
     * @param theClass the class to set the metaclass for
     * @param theMetaClass the new metaclass
     */
    @Override
    public void setMetaClass(Class theClass, MetaClass theMetaClass) {
        setMetaClass(theClass, null, theMetaClass);
    }

    /**
     * Sets the per-instance metaclass for the given object.
     * This allows individual instances to have their own custom metaclass implementation.
     *
     * @param obj the object to set the metaclass for
     * @param theMetaClass the new metaclass for this instance
     */
    public void setMetaClass(Object obj, MetaClass theMetaClass) {
        Class theClass = obj instanceof Class ? (Class)obj : obj.getClass();
        ClassInfo info = ClassInfo.getClassInfo(theClass);
        MetaClass mc;
        info.lock();
        try {
            mc = info.getPerInstanceMetaClass(obj);
            info.setPerInstanceMetaClass(obj, theMetaClass);
        }
        finally {
            info.unlock();
        }
        fireConstantMetaClassUpdate(obj, theClass, mc, theMetaClass);
    }

    /**
     * Returns whether reflection access modifiers are being set.
     * When true, the AccessibleObject.setAccessible(true) method will be called
     * when using reflection to access methods and fields.
     *
     * @return true if using accessible reflection, false otherwise
     */
    public boolean useAccessible() {
        return useAccessible;
    }

    private volatile MetaClassCreationHandle metaClassCreationHandle = new MetaClassCreationHandle();

    /**
     * Gets a handle internally used to create MetaClass implementations
     * WARNING: experimental code, likely to change soon
     * @return the handle
     */
    @Override
    public MetaClassCreationHandle getMetaClassCreationHandler() {
        return metaClassCreationHandle;
    }

    /**
     * Sets a handle internally used to create MetaClass implementations.
     * When replacing the handle with a custom version, you should
     * reuse the old handle to keep custom logic and to use the
     * default logic as fall back.
     * WARNING: experimental code, likely to change soon
     * @param handle the handle
     */
    @Override
    public void setMetaClassCreationHandle(MetaClassCreationHandle handle) {
        if(handle == null) throw new IllegalArgumentException("Cannot set MetaClassCreationHandle to null value!");
        ClassInfo.clearModifiedExpandos();
        handle.setDisableCustomMetaClassLookup(metaClassCreationHandle.isDisableCustomMetaClassLookup());
        metaClassCreationHandle = handle;
    }

    /**
     * Adds a listener for constant metaclasses.
     * @param listener the listener
     */
    @Override
    public void addMetaClassRegistryChangeEventListener(MetaClassRegistryChangeEventListener listener) {
        synchronized (changeListenerList) {
            changeListenerList.add(listener);
        }
    }


    /**
     * Adds a listener for constant metaclasses. This listener cannot be removed!
     * @param listener the listener
     */
    @Override
    public void addNonRemovableMetaClassRegistryChangeEventListener(MetaClassRegistryChangeEventListener listener) {
        synchronized (changeListenerList) {
            nonRemoveableChangeListenerList.add(listener);
        }
    }

    /**
     * Removes a constant metaclass listener.
     * @param listener the listener
     */
    @Override
    public void removeMetaClassRegistryChangeEventListener(MetaClassRegistryChangeEventListener listener) {
        synchronized (changeListenerList) {
            changeListenerList.remove(listener);
        }
    }

    /**
     * Causes the execution of all registered listeners. This method is used mostly
     * internal to kick of the listener notification. It can also be used by subclasses
     * to achieve the same.
     *
     * @param obj object instance if the MetaClass change is on a per-instance metaclass (or null if global)
     * @param c the class
     * @param oldMC the old MetaClass
     * @param newMc the new MetaClass
     */
    protected void fireConstantMetaClassUpdate(Object obj, Class c, final MetaClass oldMC, MetaClass newMc) {
        MetaClassRegistryChangeEventListener[]  listener = getMetaClassRegistryChangeEventListeners();
        MetaClassRegistryChangeEvent cmcu = new MetaClassRegistryChangeEvent(this, obj, c, oldMC, newMc);
        for (MetaClassRegistryChangeEventListener metaClassRegistryChangeEventListener : listener) {
            metaClassRegistryChangeEventListener.updateConstantMetaClass(cmcu);
        }
    }

    /**
     * Gets an array of all registered ConstantMetaClassListener instances.
     */
    @Override
    public MetaClassRegistryChangeEventListener[] getMetaClassRegistryChangeEventListeners() {
        synchronized (changeListenerList) {
            ArrayList<MetaClassRegistryChangeEventListener> ret =
                    new ArrayList<MetaClassRegistryChangeEventListener>(changeListenerList.size()+nonRemoveableChangeListenerList.size());
            ret.addAll(nonRemoveableChangeListenerList);
            ret.addAll(changeListenerList);
            return ret.toArray(EMPTY_METACLASSREGISTRYCHANGEEVENTLISTENER_ARRAY);
        }
    }

    /**
     * Singleton of MetaClassRegistry.
     *
     * @param includeExtension
     * @return the registry
     */
    public static synchronized MetaClassRegistry getInstance(int includeExtension) {
        if (includeExtension != DONT_LOAD_DEFAULT) {
            if (instanceInclude == null) {
                instanceInclude = new MetaClassRegistryImpl();
            }
            return instanceInclude;
        } else {
            if (instanceExclude == null) {
                instanceExclude = new MetaClassRegistryImpl(DONT_LOAD_DEFAULT);
            }
            return instanceExclude;
        }
    }

    /**
     * Gets the instance methods registered in this registry.
     *
     * @return a FastArray of all instance methods
     */
    public FastArray getInstanceMethods() {
        return instanceMethods;
    }

    /**
     * Gets the static methods registered in this registry.
     *
     * @return a FastArray of all static methods
     */
    public FastArray getStaticMethods() {
        return staticMethods;
    }

    /**
     * Returns an iterator to iterate over all constant metaclasses.
     * This iterator can be seen as making a snapshot of the current state
     * of the registry. The snapshot will include all metaclasses that has
     * been used unless they are already collected. Collected metaclasses
     * will be skipped automatically, so you can expect that each element
     * of the iteration is not null. Calling this method is thread safe, the
     * usage of the iterator is not.
     *
     * @return the iterator.
     */
    @Override
    public Iterator iterator() {
        final MetaClass[] refs = metaClassInfo.toArray(EMPTY_METACLASS_ARRAY);

        return new Iterator() {
            // index in the ref array
            private int index = 0;
            // the current metaclass
            private MetaClass currentMeta;
            // used to ensure that hasNext has been called
            private boolean hasNextCalled = false;
            // the cached hasNext call value
            private boolean hasNext = false;

            @Override
            public boolean hasNext() {
                if (hasNextCalled) return hasNext;
                hasNextCalled = true;
                if(index < refs.length) {
                    hasNext = true;
                    currentMeta = refs[index];
                    index++;
                } else {
                    hasNext = false;
                }
                return hasNext;
            }

            private void ensureNext() {
                // we ensure that hasNext has been called before
                // next is called
                hasNext();
                hasNextCalled = false;
            }

            @Override
            public Object next() {
                ensureNext();
                return currentMeta;
            }

            @Override
            public void remove() {
                ensureNext();
                setMetaClass(currentMeta.getTheClass(), currentMeta, null);
                currentMeta = null;
            }
        };
    }

    private class DefaultModuleListener implements ExtensionModuleScanner.ExtensionModuleListener {
        private final Map<CachedClass, List<MetaMethod>> map;

        public DefaultModuleListener(final Map<CachedClass, List<MetaMethod>> map) {
            this.map = map;
        }

        @Override
        public void onModule(final ExtensionModule module) {
            if (moduleRegistry.hasModule(module.getName())) {
                ExtensionModule loadedModule = moduleRegistry.getModule(module.getName());
                if (loadedModule.getVersion().equals(module.getVersion())) {
                    // already registered
                    return;
                } else {
                    throw new GroovyRuntimeException("Conflicting module versions. Module [" + module.getName() + " is loaded in version " +
                            loadedModule.getVersion() + " and you are trying to load version " + module.getVersion());
                }
            }
            moduleRegistry.addModule(module);
            // register MetaMethods
            List<MetaMethod> metaMethods = module.getMetaMethods();
            for (MetaMethod metaMethod : metaMethods) {
                CachedClass cachedClass = metaMethod.getDeclaringClass();
                List<MetaMethod> methods = map.computeIfAbsent(cachedClass, k -> new ArrayList<MetaMethod>(4));
                methods.add(metaMethod);
                if (metaMethod.isStatic()) {
                    staticMethods.add(metaMethod);
                } else {
                    instanceMethods.add(metaMethod);
                }
            }
        }
    }

    // DGM records use Class[]
    private boolean isDisabled(String methodName, Class[] parameters) {
        for (DisabledMethodSpec spec : disabledSpecs) {
            if (spec.matches(methodName, parameters)) return true;
        }
        return false;
    }

    // Extension module methods use CachedClass[]
    private boolean isDisabled(String methodName, CachedClass[] parameters) {
        for (DisabledMethodSpec spec : disabledSpecs) {
            if (spec.matches(methodName, parameters)) return true;
        }
        return false;
    }

    /**
     * Represents a parsed entry from the {@code groovy.extension.disable} system property.
     * <p>
     * Supported forms:
     * <ul>
     *   <li>{@code methodName} — matches all overloads</li>
     *   <li>{@code methodName(type)} — matches by receiver type (first parameter)</li>
     *   <li>{@code methodName(type1,type2,...)} — matches the exact parameter signature</li>
     * </ul>
     */
    private static final class DisabledMethodSpec {
        private final String name;
        private final String[] paramTypes; // null means match all overloads

        DisabledMethodSpec(String name, String[] paramTypes) {
            this.name = name;
            this.paramTypes = paramTypes;
        }

        boolean matches(String methodName, Class[] parameters) {
            if (!name.equals(methodName)) return false;
            if (paramTypes == null) return true;
            if (paramTypes.length != parameters.length) return false;
            for (int i = 0; i < paramTypes.length; i++) {
                if (!matchesType(paramTypes[i], parameters[i])) return false;
            }
            return true;
        }

        boolean matches(String methodName, CachedClass[] parameters) {
            if (!name.equals(methodName)) return false;
            if (paramTypes == null) return true;
            if (paramTypes.length != parameters.length) return false;
            for (int i = 0; i < paramTypes.length; i++) {
                if (!matchesType(paramTypes[i], parameters[i].getTheClass())) return false;
            }
            return true;
        }

        // Allows simple name ("Set"), fully qualified name ("java.util.Set"),
        // and array notation ("Object[]", "boolean[][]", etc.).
        // Uses Class.getCanonicalName() which produces human-readable names
        // like "java.lang.Object[]" instead of JVM names like "[Ljava.lang.Object;".
        private static boolean matchesType(String spec, Class<?> type) {
            String canonical = type.getCanonicalName();
            if (spec.equals(canonical)) return true;
            // allow simple name to match: "Set" matches "java.util.Set",
            // "Object[]" matches "java.lang.Object[]"
            String simpleName = type.getSimpleName();
            return spec.equals(simpleName);
        }

        /**
         * Parses the comma-separated disable property value, respecting parentheses.
         * For example: {@code "asChecked,toSorted(Set,Class),collect"} produces three specs.
         */
        static List<DisabledMethodSpec> parse(String input) {
            List<DisabledMethodSpec> specs = new ArrayList<>();
            for (String entry : splitRespectingParens(input)) {
                entry = entry.trim();
                if (entry.isEmpty()) continue;
                int parenIdx = entry.indexOf('(');
                if (parenIdx < 0) {
                    specs.add(new DisabledMethodSpec(entry, null));
                } else {
                    String name = entry.substring(0, parenIdx);
                    String paramStr = entry.substring(parenIdx + 1, entry.length() - 1);
                    if (paramStr.isEmpty()) {
                        specs.add(new DisabledMethodSpec(name, new String[0]));
                    } else {
                        String[] params = paramStr.split(",");
                        for (int i = 0; i < params.length; i++) params[i] = params[i].trim();
                        specs.add(new DisabledMethodSpec(name, params));
                    }
                }
            }
            return specs;
        }

        private static List<String> splitRespectingParens(String input) {
            List<String> result = new ArrayList<>();
            int depth = 0, start = 0;
            for (int i = 0; i < input.length(); i++) {
                char c = input.charAt(i);
                if (c == '(') depth++;
                else if (c == ')') depth--;
                else if (c == ',' && depth == 0) {
                    result.add(input.substring(start, i));
                    start = i + 1;
                }
            }
            result.add(input.substring(start));
            return result;
        }
    }
}
