/*
 *  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.Closure;
import groovy.lang.ExpandoMetaClass;
import groovy.lang.GroovyInterceptable;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MetaBeanProperty;
import groovy.lang.MetaClass;
import groovy.lang.MetaClassImpl;
import groovy.lang.MetaClassRegistry;
import groovy.lang.MetaMethod;
import groovy.lang.MetaProperty;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import groovy.lang.ProxyMetaClass;
import org.codehaus.groovy.reflection.CachedClass;
import org.codehaus.groovy.reflection.CachedField;
import org.codehaus.groovy.reflection.CachedMethod;
import org.codehaus.groovy.reflection.ParameterTypes;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.MetaClassHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.callsite.PogoMetaClassSite;
import org.codehaus.groovy.runtime.wrappers.Wrapper;
import org.codehaus.groovy.util.FastArray;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A metaclass for closures generated by the Groovy compiler. These classes
 * have special characteristics this MetaClass uses. One of these is that a
 * generated Closure has only additional doCall methods, all other methods
 * are in the Closure class as well. To use this fact this MetaClass uses
 * a MetaClass for Closure as static field And delegates calls to this
 * MetaClass if needed. This allows a lean implementation for this MetaClass.
 * Multiple generated closures will then use the same MetaClass for Closure.
 * For static dispatching this class uses the MetaClass of Class, again
 * all instances of this class will share that MetaClass. The Class MetaClass
 * is initialized lazy, because most operations do not need this MetaClass.
 * <p>
 * The Closure and Class MetaClasses are not replaceable.
 * <p>
 * This MetaClass is for internal usage only!
 *
 * @since 1.5
 */
public final class ClosureMetaClass extends MetaClassImpl {
    private volatile boolean initialized, attributeInitDone;
    private final FastArray closureMethods = new FastArray(3);
    private final Map<String, CachedField> attributes = new HashMap<>();
    private MethodChooser chooser;

    private static MetaClassImpl classMetaClass;
    private static MetaClassImpl CLOSURE_METACLASS;
    private static final Object[] EMPTY_ARGUMENTS = {};
    private static final String CLOSURE_CALL_METHOD = "call";
    private static final String CLOSURE_DO_CALL_METHOD = "doCall";

    static {
        resetCachedMetaClasses();
    }

    public static void resetCachedMetaClasses() {
        MetaClassImpl temp = new MetaClassImpl(Closure.class);
        temp.initialize();
        synchronized (ClosureMetaClass.class) {
            CLOSURE_METACLASS = temp;
        }
        if (classMetaClass != null) {
            temp = new MetaClassImpl(Class.class);
            temp.initialize();
            synchronized (ClosureMetaClass.class) {
                classMetaClass = temp;
            }
        }
    }

    private static synchronized MetaClass getStaticMetaClass() {
        if (classMetaClass == null) {
            classMetaClass = new MetaClassImpl(Class.class);
            classMetaClass.initialize();
        }
        return classMetaClass;
    }

    private interface MethodChooser {
        Object chooseMethod(Class[] arguments, boolean coerce);
    }

    private static class StandardClosureChooser implements MethodChooser {
        private final MetaMethod doCall0;
        private final MetaMethod doCall1;

        StandardClosureChooser(final MetaMethod m0, final MetaMethod m1) {
            doCall0 = m0;
            doCall1 = m1;
        }

        @Override
        public Object chooseMethod(final Class[] arguments, final boolean coerce) {
            if (arguments.length == 0) return doCall0;
            if (arguments.length == 1) return doCall1;
            return null;
        }
    }

    private static class NormalMethodChooser implements MethodChooser {
        private final FastArray methods;
        final Class theClass;

        NormalMethodChooser(final Class theClass, final FastArray methods) {
            this.theClass = theClass;
            this.methods = methods;
        }

        @Override
        public Object chooseMethod(final Class[] arguments, final boolean coerce) {
            if (arguments.length == 0) {
                return MetaClassHelper.chooseEmptyMethodParams(methods);
            } else if (arguments.length == 1 && arguments[0] == null) {
                return MetaClassHelper.chooseMostGeneralMethodWith1NullParam(methods);
            } else {
                List matchingMethods = new ArrayList();
                final Object[] data = methods.getArray();
                for (int i = 0, n = methods.size(); i < n; i += 1) {
                    Object method = data[i];

                    // making this false helps find matches
                    if (((ParameterTypes) method).isValidMethod(arguments)) {
                        matchingMethods.add(method);
                    }
                }

                int size = matchingMethods.size();
                if (0 == size) {
                    return null;
                } else if (1 == size) {
                    return matchingMethods.get(0);
                }

                return chooseMostSpecificParams(CLOSURE_DO_CALL_METHOD, matchingMethods, arguments);
            }
        }

        private Object chooseMostSpecificParams(final String name, final List matchingMethods, final Class[] arguments) {
            return doChooseMostSpecificParams(theClass.getName(), name, matchingMethods, arguments, true);
        }
    }

    //--------------------------------------------------------------------------

    public ClosureMetaClass(final MetaClassRegistry registry, final Class theClass) {
        super(registry, theClass);
    }

    @Override
    public MetaProperty getMetaProperty(final String name) {
        return CLOSURE_METACLASS.getMetaProperty(name);
    }

    private static void unwrap(final Object[] arguments) {
        for (int i = 0; i != arguments.length; i++) {
            if (arguments[i] instanceof Wrapper) {
                arguments[i] = ((Wrapper) arguments[i]).unwrap();
            }
        }
    }

    private MetaMethod pickClosureMethod(final Class[] argClasses) {
        Object answer = chooser.chooseMethod(argClasses, false);
        return (MetaMethod) answer;
    }

    private MetaMethod getDelegateMethod(final Closure closure, final Object delegate, final String methodName, final Class[] argClasses) {
        if (delegate == closure || delegate == null) return null;
        if (delegate instanceof Class) {
            for (Class superClass = (Class) delegate;
                 superClass != Object.class && superClass != null;
                 superClass = superClass.getSuperclass())
            {
                MetaClass mc = registry.getMetaClass(superClass);
                MetaMethod method = mc.getStaticMetaMethod(methodName, argClasses);
                if (method != null) return method;
            }
            return null;
        } else if (delegate instanceof GroovyInterceptable) {
            MetaClass delegateMetaClass = lookupObjectMetaClass(delegate);
            // GROOVY-3015: must route calls through GroovyObject#invokeMethod(String,Object)
            MetaMethod interceptMethod = delegateMetaClass.pickMethod("invokeMethod", new Class[]{String.class, Object.class});
            return new TransformMetaMethod(interceptMethod) {
                @Override
                public Object invoke(final Object object, final Object[] arguments) {
                    return super.invoke(object, new Object[]{methodName, arguments});
                }
            };
        } else {
            MetaClass delegateMetaClass = lookupObjectMetaClass(delegate);
            MetaMethod method = delegateMetaClass.pickMethod(methodName, argClasses);
            if (method != null) {
                return method;
            }

            if (delegateMetaClass instanceof ExpandoMetaClass) {
                method = ((ExpandoMetaClass) delegateMetaClass).findMixinMethod(methodName, argClasses);

                if (method != null) {
                    onMixinMethodFound(method);
                    return method;
                }
            }

            if (delegateMetaClass instanceof MetaClassImpl) {
                method = MetaClassImpl.findMethodInClassHierarchy(getTheClass(), methodName, argClasses, this);
                if (method != null) {
                    onSuperMethodFoundInHierarchy(method);
                    return method;
                }
            }

            return method;
        }
    }

    @Override
    public Object invokeMethod(final Class sender, final Object object, final String methodName, final Object[] originalArguments, final boolean isCallToSuper, final boolean fromInsideClass) {
        checkInitalised();
        if (object == null) {
            throw new NullPointerException("Cannot invoke method: " + methodName + " on null object");
        }

        final Object[] arguments = makeArguments(originalArguments, methodName);
        final Class<?>[] argClasses = MetaClassHelper.convertToTypeArray(arguments);
        unwrap(arguments);

        MetaMethod method = null;
        final Closure<?> closure = (Closure<?>) object;
        final int resolveStrategy = closure.getResolveStrategy();

        if (CLOSURE_DO_CALL_METHOD.equals(methodName) || CLOSURE_CALL_METHOD.equals(methodName)) {
            method = pickClosureMethod(argClasses);
            if (method == null && arguments.length == 1 && arguments[0] instanceof List) {
                Object[] newArguments = ((List<?>) arguments[0]).toArray();
                Class<?>[] newArgClasses = MetaClassHelper.convertToTypeArray(newArguments);
                method = createTransformMetaMethod(pickClosureMethod(newArgClasses));
            }
            if (method == null) throw new MissingMethodException(methodName, theClass, arguments, false);
        }

        boolean shouldDefer = resolveStrategy == Closure.DELEGATE_ONLY && isInternalMethod(methodName);
        if (method == null && !shouldDefer) {
            method = CLOSURE_METACLASS.pickMethod(methodName, argClasses);
        }

        if (method != null) return method.doMethodInvoke(object, arguments);

        MissingMethodException last = null;
        Object callObject = object;
        final Object owner = closure.getOwner();
        final Object delegate = closure.getDelegate();
        boolean invokeOnDelegate = false;
        boolean invokeOnOwner = false;
        boolean ownerFirst = true;

        switch (resolveStrategy) {
            case Closure.TO_SELF:
                break;
            case Closure.DELEGATE_ONLY:
                method = getDelegateMethod(closure, delegate, methodName, argClasses);
                callObject = delegate;
                if (method == null) {
                    invokeOnDelegate = delegate != closure && (delegate instanceof GroovyObject);
                }
                break;
            case Closure.OWNER_ONLY:
                method = getDelegateMethod(closure, owner, methodName, argClasses);
                callObject = owner;
                if (method == null) {
                    invokeOnOwner = owner != closure && (owner instanceof GroovyObject);
                }
                break;
            case Closure.DELEGATE_FIRST:
                method = getDelegateMethod(closure, delegate, methodName, argClasses);
                callObject = delegate;
                if (method == null) {
                    invokeOnDelegate = delegate != closure;
                    invokeOnOwner = owner != closure;
                    ownerFirst = false;
                }
                break;
            default: // Closure.OWNER_FIRST:
                method = getDelegateMethod(closure, owner, methodName, argClasses);
                callObject = owner;
                if (method == null) {
                    invokeOnDelegate = delegate != closure;
                    invokeOnOwner = owner != closure;
                    ownerFirst = true;
                }
                break;
        }
        if (method == null && (invokeOnOwner || invokeOnDelegate)) {
            try {
                if (ownerFirst) {
                    return invokeOnDelegationObjects(invokeOnOwner, owner, invokeOnDelegate, delegate, methodName, arguments);
                } else {
                    return invokeOnDelegationObjects(invokeOnDelegate, delegate, invokeOnOwner, owner, methodName, arguments);
                }
            } catch (MissingMethodException mme) {
                last = mme;
            }
        }

        if (method != null) {
            MetaClass metaClass = registry.getMetaClass(callObject.getClass());
            if (metaClass instanceof ProxyMetaClass) {
                return metaClass.invokeMethod(callObject, methodName, arguments);
            } else {
                return method.doMethodInvoke(callObject, arguments);
            }
        } else {
            // no method was found; try to find a closure defined as a field of the class and run it
            Object value = null;
            try {
                value = getProperty(sender, object, methodName, isCallToSuper, fromInsideClass);
            } catch (MissingPropertyException mpe) {
                // ignore
            }
            if (value instanceof Closure) {  // This test ensures that value != this If you ever change this ensure that value != this
                Closure<?> cl = (Closure<?>) value;
                MetaClass delegateMetaClass = cl.getMetaClass();
                return delegateMetaClass.invokeMethod(cl.getClass(), closure, CLOSURE_DO_CALL_METHOD, originalArguments, false, fromInsideClass);
            }
        }

        throw last != null ? last : new MissingMethodException(methodName, theClass, arguments, false);
    }

    private static boolean isInternalMethod(final String methodName) {
        switch (methodName) {
            case "curry":
            case "ncurry":
            case "rcurry":
            case "leftShift":
            case "rightShift":
                return true;
            default:
                return false;
        }
    }

    private static Object[] makeArguments(final Object[] arguments, final String methodName) {
        if (arguments == null) return EMPTY_ARGUMENTS;
        return arguments;
    }

    private static Throwable unwrap(final GroovyRuntimeException gre) {
        Throwable th = gre;
        if (th.getCause() != null && th.getCause() != gre) th = th.getCause();
        if (th != gre && (th instanceof GroovyRuntimeException)) return unwrap((GroovyRuntimeException) th);
        return th;
    }

    private static Object invokeOnDelegationObjects(final boolean invoke1, final Object o1, final boolean invoke2, final Object o2, final String methodName, final Object[] args) {
        MissingMethodException first = null;
        if (invoke1) {
            try {
                return InvokerHelper.invokeMethod(o1, methodName, args);
            } catch (MissingMethodException mme) {
                first = mme;
            } catch (GroovyRuntimeException gre) {
                Throwable t = unwrap(gre);
                if (t instanceof MissingMethodException && methodName.equals(((MissingMethodException) t).getMethod())) {
                    first = (MissingMethodException) t;
                } else {
                    throw gre;
                }
            }
        }
        if (invoke2 && (!invoke1 || o1 != o2)) {
            try {
                return InvokerHelper.invokeMethod(o2, methodName, args);
            } catch (MissingMethodException mme) {
                // patch needed here too, but we need a test case to trip it first
                if (first == null) first = mme;
            } catch (GroovyRuntimeException gre) {
                Throwable t = unwrap(gre);
                if (t instanceof MissingMethodException) {
                    first = (MissingMethodException) t;
                } else {
                    throw gre;
                }
            }
        }
        throw first;
    }

    private synchronized void initAttributes() {
        if (!attributes.isEmpty()) return;
        attributes.put("!", null); // just a dummy for later
        CachedField[] fieldArray = theCachedClass.getFields();
        for (CachedField aFieldArray : fieldArray) {
            attributes.put(aFieldArray.getName(), aFieldArray);
        }
        attributeInitDone = !attributes.isEmpty();
    }

    @Override
    public synchronized void initialize() {
        if (!isInitialized()) {
            CachedMethod[] methodArray = theCachedClass.getMethods();
            synchronized (theCachedClass) {
                for (final CachedMethod cachedMethod : methodArray) {
                    if (!cachedMethod.getName().equals(CLOSURE_DO_CALL_METHOD)) continue;
                    closureMethods.add(cachedMethod);
                }
            }
            assignMethodChooser();
            setInitialized(true);
        }
    }

    private void assignMethodChooser() {
        if (closureMethods.size() == 1) {
            final MetaMethod doCall = (MetaMethod) closureMethods.get(0);
            final CachedClass[] c = doCall.getParameterTypes();
            int length = c.length;
            if (length == 0) {
                // no arg method
                chooser = (arguments, coerce) -> {
                    if (arguments.length == 0) return doCall;
                    return null;
                };
            } else {
                if (length == 1 && c[0].getTheClass() == Object.class) {
                    // Object fits all, so simple dispatch rule here
                    chooser = (arguments, coerce) -> {
                        // <2, because foo() is same as foo(null)
                        if (arguments.length < 2) return doCall;
                        return null;
                    };
                } else {
                    boolean allObject = true;
                    for (int i = 0; i < c.length - 1; i++) {
                        if (c[i].getTheClass() != Object.class) {
                            allObject = false;
                            break;
                        }
                    }
                    if (allObject && c[c.length - 1].getTheClass() == Object.class) {
                        // all arguments are object, so test only if argument number is correct
                        chooser = (arguments, coerce) -> {
                            if (arguments.length == c.length) return doCall;
                            return null;
                        };
                    } else {
                        if (allObject && c[c.length - 1].getTheClass() == Object[].class) {
                            // all arguments are Object but last, which is a vargs argument, that
                            // will fit all, so just test if the number of argument is equal or
                            // more than the parameters we have.
                            final int minimumLength = c.length - 2;
                            chooser = (arguments, coerce) -> {
                                if (arguments.length > minimumLength) return doCall;
                                return null;
                            };
                        } else {
                            // general case for single method
                            chooser = (arguments, coerce) -> {
                                if (doCall.isValidMethod(arguments)) {
                                    return doCall;
                                }
                                return null;
                            };
                        }
                    }
                }
            }
        } else if (closureMethods.size() == 2) {
            MetaMethod m0 = null, m1 = null;
            for (int i = 0; i != closureMethods.size(); ++i) {
                MetaMethod m = (MetaMethod) closureMethods.get(i);
                CachedClass[] c = m.getParameterTypes();
                if (c.length == 0) {
                    m0 = m;
                } else {
                    if (c.length == 1 && c[0].getTheClass() == Object.class) {
                        m1 = m;
                    }
                }
            }
            if (m0 != null && m1 != null) {
                // standard closure (2 methods because "it" is with default null)
                chooser = new StandardClosureChooser(m0, m1);
            }
        }
        if (chooser == null) {
            // standard chooser for cases if it is not a single method and if it is
            // not the standard closure.
            chooser = new NormalMethodChooser(theClass, closureMethods);
        }
    }

    private MetaClass lookupObjectMetaClass(final Object object) {
        if (object instanceof GroovyObject) {
            GroovyObject go = (GroovyObject) object;
            return go.getMetaClass();
        }
        Class ownerClass = object.getClass();
        if (ownerClass == Class.class) {
            ownerClass = (Class) object;
            return registry.getMetaClass(ownerClass);
        }
        MetaClass metaClass = InvokerHelper.getMetaClass(object);
        return metaClass;
    }

    @Override
    public List<MetaMethod> getMethods() {
        List<MetaMethod> answer = CLOSURE_METACLASS.getMetaMethods();
        answer.addAll(closureMethods.toList());
        return answer;
    }

    @Override
    public List<MetaMethod> getMetaMethods() {
        return CLOSURE_METACLASS.getMetaMethods();
    }

    @Override
    public List<MetaProperty> getProperties() {
        return CLOSURE_METACLASS.getProperties();
    }

    @Override
    public MetaMethod pickMethod(final String methodName, final Class[] argumentTypes) {
        if (CLOSURE_CALL_METHOD.equals(methodName) || CLOSURE_DO_CALL_METHOD.equals(methodName)) {
            return pickClosureMethod(argumentTypes != null ? argumentTypes : MetaClassHelper.EMPTY_CLASS_ARRAY);
        }
        return CLOSURE_METACLASS.getMetaMethod(methodName, argumentTypes);
    }

    public MetaMethod retrieveStaticMethod(final String methodName, final Class[] arguments) {
        return null;
    }

    @Override
    protected boolean isInitialized() {
        return initialized;
    }

    @Override
    protected void setInitialized(final boolean initialized) {
        this.initialized = initialized;
    }

    @Override
    public MetaMethod getStaticMetaMethod(final String name, final Object[] args) {
        return CLOSURE_METACLASS.getStaticMetaMethod(name, args);
    }

    public MetaMethod getStaticMetaMethod(final String name, final Class[] argTypes) {
        return CLOSURE_METACLASS.getStaticMetaMethod(name, argTypes);
    }

    @Override
    public Object getProperty(final Class sender, final Object object, final String name, final boolean useSuper, final boolean fromInsideClass) {
        if (object instanceof Class) {
            return getStaticMetaClass().getProperty(sender, object, name, useSuper, fromInsideClass);
        } else {
            return CLOSURE_METACLASS.getProperty(sender, object, name, useSuper, fromInsideClass);
        }
    }

    @Override
    public Object getAttribute(final Class sender, final Object object, final String attribute, final boolean useSuper, final boolean fromInsideClass) {
        if (object instanceof Class) {
            return getStaticMetaClass().getAttribute(sender, object, attribute, useSuper);
        } else {
            if (!attributeInitDone) initAttributes();
            CachedField mfp = attributes.get(attribute);
            if (mfp == null) {
                return CLOSURE_METACLASS.getAttribute(sender, object, attribute, useSuper);
            } else {
                return mfp.getProperty(object);
            }
        }
    }

    @Override
    public void setAttribute(final Class sender, final Object object, final String attribute, final Object newValue, final boolean useSuper, final boolean fromInsideClass) {
        if (object instanceof Class) {
            getStaticMetaClass().setAttribute(sender, object, attribute, newValue, useSuper, fromInsideClass);
        } else {
            if (!attributeInitDone) initAttributes();
            CachedField mfp = attributes.get(attribute);
            if (mfp == null) {
                CLOSURE_METACLASS.setAttribute(sender, object, attribute, newValue, useSuper, fromInsideClass);
            } else {
                mfp.setProperty(object, newValue);
            }
        }
    }

    @Override
    public Object invokeStaticMethod(final Object object, final String methodName, final Object[] arguments) {
        return getStaticMetaClass().invokeMethod(Class.class, object, methodName, arguments, false, false);
    }

    @Override
    public void setProperty(final Class sender, final Object object, final String name, final Object newValue, final boolean useSuper, final boolean fromInsideClass) {
        if (object instanceof Class) {
            getStaticMetaClass().setProperty(sender, object, name, newValue, useSuper, fromInsideClass);
        } else {
            CLOSURE_METACLASS.setProperty(sender, object, name, newValue, useSuper, fromInsideClass);
        }
    }

    public MetaMethod getMethodWithoutCaching(final int index, final Class sender, final String methodName, final Class[] arguments, final boolean isCallToSuper) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void setProperties(final Object bean, final Map map) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void addMetaBeanProperty(final MetaBeanProperty mp) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void addMetaMethod(final MetaMethod method) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void addNewInstanceMethod(final Method method) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void addNewStaticMethod(final Method method) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Constructor retrieveConstructor(final Class[] arguments) {
        throw new UnsupportedOperationException();
    }

    @Override
    public CallSite createPojoCallSite(final CallSite site, final Object receiver, final Object[] args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public CallSite createPogoCallSite(final CallSite site, final Object[] args) {
        return new PogoMetaClassSite(site, this);
    }

    @Override
    public CallSite createPogoCallCurrentSite(final CallSite site, final Class sender, final Object[] args) {
        return new PogoMetaClassSite(site, this);
    }

    @Override
    public List respondsTo(final Object obj, final String name, final Object[] argTypes) {
        loadMetaInfo();
        return super.respondsTo(obj, name, argTypes);
    }

    @Override
    public List respondsTo(final Object obj, final String name) {
        loadMetaInfo();
        return super.respondsTo(obj, name);
    }

    private synchronized void loadMetaInfo() {
        if (metaMethodIndex.isEmpty()) {
          reinitialize();
        }
    }

    @Override
    protected void applyPropertyDescriptors(final PropertyDescriptor[] propertyDescriptors) {
        // do nothing
    }
}
