/*
 *  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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * 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];
    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 Set<String> disabledNames = disabling ? new HashSet<>(Arrays.asList(disabledString.split(","))) : null;

    public static final int LOAD_DEFAULT = 0;
    public static final int DONT_LOAD_DEFAULT = 1;
    private static MetaClassRegistry instanceInclude;
    private static MetaClassRegistry instanceExclude;

    public MetaClassRegistryImpl() {
        this(LOAD_DEFAULT, true);
    }

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

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

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

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

    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
     */
    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 && disabledNames.contains(record.methodName)) 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) {
                e.printStackTrace();
                // 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 && disabledNames.contains(method.getName())) 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 */
        }
    }

    @Override
    public final MetaClass getMetaClass(Class theClass) {
        return ClassInfo.getClassInfo(theClass).getMetaClass();
    }

    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 = null;
        info.lock();
        try {
            mc = info.getStrongMetaClass();
            info.setStrongMetaClass(newMC);
        } finally {
            info.unlock();
        }
        if ((oldMC == null && mc != newMC) || (oldMC != null && mc != newMC && mc != oldMC)) {
            fireConstantMetaClassUpdate(null, theClass, mc, newMC);
        }
    }

    @Override
    public void removeMetaClass(Class theClass) {
        setMetaClass(theClass, null, null);
    }

    @Override
    public void setMetaClass(Class theClass, MetaClass theMetaClass) {
        setMetaClass(theClass, null, theMetaClass);
    }

    public void setMetaClass(Object obj, MetaClass theMetaClass) {
        Class theClass = obj instanceof Class ? (Class)obj : obj.getClass();
        ClassInfo info = ClassInfo.getClassInfo(theClass);
        MetaClass mc = null;
        info.lock();
        try {
            mc = info.getPerInstanceMetaClass(obj);
            info.setPerInstanceMetaClass(obj, theMetaClass);
        }
        finally {
            info.unlock();
        }
        fireConstantMetaClassUpdate(obj, theClass, mc, theMetaClass);
    }

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

    public FastArray getInstanceMethods() {
        return instanceMethods;
    }

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