/*
 *  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 groovy.util;

import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.DelegatingMetaClass;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MetaClass;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import groovy.lang.Reference;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.apache.groovy.util.BeanUtils.capitalize;

/**
 * Mix of BuilderSupport and SwingBuilder's factory support.
 *
 * Warning: this implementation is not thread safe and should not be used
 * across threads in a multithreaded environment.  A locking mechanism
 * should be implemented by the subclass if use is expected across
 * multiple threads.
 */
public abstract class FactoryBuilderSupport extends Binding {
    public static final String CURRENT_FACTORY = "_CURRENT_FACTORY_";
    public static final String PARENT_FACTORY = "_PARENT_FACTORY_";
    public static final String PARENT_NODE = "_PARENT_NODE_";
    public static final String CURRENT_NODE = "_CURRENT_NODE_";
    public static final String PARENT_CONTEXT = "_PARENT_CONTEXT_";
    public static final String PARENT_NAME = "_PARENT_NAME_";
    public static final String CURRENT_NAME = "_CURRENT_NAME_";
    public static final String OWNER = "owner";
    public static final String PARENT_BUILDER = "_PARENT_BUILDER_";
    public static final String CURRENT_BUILDER = "_CURRENT_BUILDER_";
    public static final String CHILD_BUILDER = "_CHILD_BUILDER_";
    public static final String SCRIPT_CLASS_NAME = "_SCRIPT_CLASS_NAME_";
    private static final Logger LOG = Logger.getLogger(FactoryBuilderSupport.class.getName());
    private static final Comparator<Method> METHOD_COMPARATOR = Comparator.comparing(Method::getName).thenComparingInt(o -> o.getParameterTypes().length);

    /**
     * Throws an exception if value is null.
     *
     * @param value the node's value
     * @param name  the node's name
     */
    public static void checkValueIsNull(Object value, Object name) {
        if (value != null) {
            throw new RuntimeException("'" + name + "' elements do not accept a value argument.");
        }
    }

    /**
     * Checks type of value against builder type
     *
     * @param value the node's value
     * @param name  the node's name
     * @param type  a Class that may be assignable to the value's class
     * @return true if type is assignable to the value's class, false if value
     *         is null.
     */
    public static boolean checkValueIsType(Object value, Object name, Class type) {
        if (value != null) {
            if (type.isAssignableFrom(value.getClass())) {
                return true;
            } else {
                throw new RuntimeException("The value argument of '" + name + "' must be of type "
                        + type.getName() + ". Found: " + value.getClass());
            }
        } else {
            return false;
        }
    }

    /**
     * Checks values against factory's type
     *
     * @param value the node's value
     * @param name  the node's name
     * @param type  a Class that may be assignable to the value's class
     * @return Returns true if type is assignable to the value's class, false if value is
     *         null or a String.
     */
    public static boolean checkValueIsTypeNotString(Object value, Object name, Class type) {
        if (value != null) {
            if (type.isAssignableFrom(value.getClass())) {
                return true;
            } else if (value instanceof String) {
                return false;
            } else {
                throw new RuntimeException("The value argument of '" + name + "' must be of type "
                        + type.getName() + " or a String. Found: " + value.getClass());
            }
        } else {
            return false;
        }
    }

    private final ThreadLocal<LinkedList<Map<String, Object>>> contexts = new ThreadLocal<LinkedList<Map<String, Object>>>();
    protected LinkedList<Closure> attributeDelegates = new LinkedList<Closure>(); //
    private final List<Closure> disposalClosures = new ArrayList<Closure>(); // because of reverse iteration use ArrayList
    private final Map<String, Factory> factories = new HashMap<String, Factory>();
    private Closure nameMappingClosure;
    private final ThreadLocal<FactoryBuilderSupport> localProxyBuilder = new ThreadLocal<FactoryBuilderSupport>();
    private FactoryBuilderSupport globalProxyBuilder;
    protected LinkedList<Closure> preInstantiateDelegates = new LinkedList<Closure>();
    protected LinkedList<Closure> postInstantiateDelegates = new LinkedList<Closure>();
    protected LinkedList<Closure> postNodeCompletionDelegates = new LinkedList<Closure>();
    protected Closure methodMissingDelegate;
    protected Closure propertyMissingDelegate;
    protected Map<String, Closure[]> explicitProperties = new HashMap<String, Closure[]>();
    protected Map<String, Closure> explicitMethods = new HashMap<String, Closure>();
    protected Map<String, Set<String>> registrationGroup = new HashMap<String, Set<String>>();
    protected String registrationGroupName = ""; // use binding to store?

    protected boolean autoRegistrationRunning = false;
    protected boolean autoRegistrationComplete = false;

    public FactoryBuilderSupport() {
        this(false);
    }

    public FactoryBuilderSupport(boolean init) {
        globalProxyBuilder = this;
        registrationGroup.put(registrationGroupName, new TreeSet<String>());
        if (init) {
            autoRegisterNodes();
        }
    }

    private Set<String> getRegistrationGroup(String name) {
        Set<String> group = registrationGroup.computeIfAbsent(name, k -> new TreeSet<String>());
        return group;
    }

    /**
     * Ask the nodes to be registered
     */
    public void autoRegisterNodes() {
        // if java did atomic blocks, this would be one
        synchronized (this) {
            if (autoRegistrationRunning || autoRegistrationComplete) {
                // registration already done or in process, abort
                return;
            }
        }
        autoRegistrationRunning = true;
        try {
            callAutoRegisterMethods(getClass());
        } finally {
            autoRegistrationComplete = true;
            autoRegistrationRunning = false;
        }
    }

    private void callAutoRegisterMethods(Class declaredClass) {
        if (declaredClass == null) {
            return;
        }
        callAutoRegisterMethods(declaredClass.getSuperclass());

        Method[] declaredMethods = declaredClass.getDeclaredMethods();
        Arrays.sort(declaredMethods, METHOD_COMPARATOR);
        for (Method method : declaredMethods) {
            if (method.getName().startsWith("register") && method.getParameterTypes().length == 0) {
                registrationGroupName = method.getName().substring("register".length());
                registrationGroup.put(registrationGroupName, new TreeSet<String>());
                try {
                    if (Modifier.isPublic(method.getModifiers())) {
                        method.invoke(this);
                    }
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("Could not init " + getClass().getName() + " because of an access error in " + declaredClass.getName() + "." + method.getName(), e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException("Could not init " + getClass().getName() + " because of an exception in " + declaredClass.getName() + "." + method.getName(), e);
                } finally {
                    registrationGroupName = "";
                }
            }
        }
    }

    /**
     * @param name the name of the variable to lookup
     * @return the variable value
     */
    @Override
    public Object getVariable(String name) {
        try {
            return getProxyBuilder().doGetVariable(name);
        } catch(MissingPropertyException mpe) {
            if(mpe.getProperty().equals(name) && propertyMissingDelegate != null) {
                return propertyMissingDelegate.call(new Object[]{name});
            }
            throw mpe;
        }
    }

    private Object doGetVariable(String name) {
        return super.getVariable(name);
    }

    /**
     * Sets the value of the given variable
     *
     * @param name  the name of the variable to set
     * @param value the new value for the given variable
     */
    @Override
    public void setVariable(String name, Object value) {
        getProxyBuilder().doSetVariable(name, value);
    }

    private void doSetVariable(String name, Object value) {
        super.setVariable(name, value);
    }

    @Override
    public Map getVariables() {
        return getProxyBuilder().doGetVariables();
    }

    private Map doGetVariables() {
        return super.getVariables();
    }

    /**
     * Overloaded to make variables appear as bean properties or via the subscript operator
     */
    @Override
    public Object getProperty(String property) {
        try {
            return getProxyBuilder().doGetProperty(property);
        } catch (MissingPropertyException mpe) {
            if ((getContext() != null) && (getContext().containsKey(property))) {
                return getContext().get(property);
            } else {
                try {
                    return getMetaClass().getProperty(this, property);
                } catch(MissingPropertyException mpe2) {
                    if(mpe2.getProperty().equals(property) && propertyMissingDelegate != null) {
                        return propertyMissingDelegate.call(new Object[]{property});
                    }
                    throw mpe2;
                }
            }
        }
    }

    private Object doGetProperty(String property) {
        Closure[] accessors = resolveExplicitProperty(property);
        if (accessors != null) {
            if (accessors[0] == null) {
                // write only property
                throw new MissingPropertyException(property + " is declared as write only");
            } else {
                return accessors[0].call();
            }
        } else {
            return super.getProperty(property);
        }
    }

    /**
     * Overloaded to make variables appear as bean properties or via the subscript operator
     */
    @Override
    public void setProperty(String property, Object newValue) {
        getProxyBuilder().doSetProperty(property, newValue);
    }

    private void doSetProperty(String property, Object newValue) {
        Closure[] accessors = resolveExplicitProperty(property);
        if (accessors != null) {
            if (accessors[1] == null) {
                // read only property
                throw new MissingPropertyException(property + " is declared as read only");
            } else {
                accessors[1].call(newValue);
            }
        } else {
            super.setProperty(property, newValue);
        }
    }

    /**
     * @return the factory map (Unmodifiable Map).
     */
    public Map<String, Factory> getFactories() {
        return Collections.unmodifiableMap(getProxyBuilder().factories);
    }

    /**
     * @return the explicit methods map (Unmodifiable Map).
     */
    public Map<String, Closure> getExplicitMethods() {
        return Collections.unmodifiableMap(getProxyBuilder().explicitMethods);
    }

    /**
     * @return the explicit properties map (Unmodifiable Map).
     */
    public Map<String, Closure[]> getExplicitProperties() {
        return Collections.unmodifiableMap(getProxyBuilder().explicitProperties);
    }

    /**
     * @return the factory map (Unmodifiable Map).
     */
    public Map<String, Factory> getLocalFactories() {
        return Collections.unmodifiableMap(factories);
    }

    /**
     * @return the explicit methods map (Unmodifiable Map).
     */
    public Map<String, Closure> getLocalExplicitMethods() {
        return Collections.unmodifiableMap(explicitMethods);
    }

    /**
     * @return the explicit properties map (Unmodifiable Map).
     */
    public Map<String, Closure[]> getLocalExplicitProperties() {
        return Collections.unmodifiableMap(explicitProperties);
    }

    public Set<String> getRegistrationGroups() {
        return Collections.unmodifiableSet(registrationGroup.keySet());
    }

    public Set<String> getRegistrationGroupItems(String group) {
        Set<String> groupSet = registrationGroup.get(group);
        if (groupSet != null) {
            return Collections.unmodifiableSet(groupSet);
        } else {
            return Collections.emptySet();
        }
    }

    public List<Closure> getAttributeDelegates() {
        return Collections.unmodifiableList(attributeDelegates);
    }

    public List<Closure> getPreInstantiateDelegates() {
        return Collections.unmodifiableList(preInstantiateDelegates);
    }

    public List<Closure> getPostInstantiateDelegates() {
        return Collections.unmodifiableList(postInstantiateDelegates);
    }

    public List<Closure> getPostNodeCompletionDelegates() {
        return Collections.unmodifiableList(postNodeCompletionDelegates);
    }

    public Closure getMethodMissingDelegate() {
        return methodMissingDelegate;
    }

    public void setMethodMissingDelegate(Closure delegate) {
        methodMissingDelegate = delegate;
    }

    public Closure getPropertyMissingDelegate() {
        return propertyMissingDelegate;
    }

    public void setPropertyMissingDelegate(Closure delegate) {
        propertyMissingDelegate = delegate;
    }

    /**
     * @return the context of the current node.
     */
    public Map<String, Object> getContext() {
        LinkedList<Map<String, Object>> contexts = getProxyBuilder().contexts.get();
        if (contexts != null && !contexts.isEmpty()) {
            return contexts.getFirst();
        }
        return null;
    }

    /**
     * @return the current node being built.
     */
    public Object getCurrent() {
        return getContextAttribute(CURRENT_NODE);
    }

    /**
     * @return the factory that built the current node.
     */
    public Factory getCurrentFactory() {
        return (Factory) getContextAttribute(CURRENT_FACTORY);
    }

    /**
     * @return the factory of the parent of the current node.
     */
    public String getCurrentName() {
        return (String) getContextAttribute(CURRENT_NAME);
    }

    /**
     * @return the builder that built the current node.
     */
    public FactoryBuilderSupport getCurrentBuilder() {
        return (FactoryBuilderSupport) getContextAttribute(CURRENT_BUILDER);
    }

    /**
     * @return the node of the parent of the current node.
     */
    public Object getParentNode() {
        return getContextAttribute(PARENT_NODE);
    }

    /**
     * @return the factory of the parent of the current node.
     */
    public Factory getParentFactory() {
        return (Factory) getContextAttribute(PARENT_FACTORY);
    }

    /**
     * @return the context of the parent of the current node.
     */
    public Map getParentContext() {
        return (Map) getContextAttribute(PARENT_CONTEXT);
    }

    /**
     * @return the name of the parent of the current node.
     */
    public String getParentName() {
        return (String) getContextAttribute(PARENT_NAME);
    }

    public FactoryBuilderSupport getChildBuilder() {
        return (FactoryBuilderSupport) getContextAttribute(CHILD_BUILDER);
    }

    public Object getContextAttribute(String key) {
        Map context = getContext();
        if (context != null) {
            return context.get(key);
        }
        return null;
    }

    /**
     * Convenience method when no arguments are required
     *
     * @param methodName the name of the method to invoke
     * @return the result of the call
     */
    public Object invokeMethod(String methodName) {
        return getProxyBuilder().invokeMethod(methodName, null);
    }

    @Override
    public Object invokeMethod(String methodName, Object args) {
        Object name = getProxyBuilder().getName(methodName);
        Object result;
        Object previousContext = getProxyBuilder().getContext();
        try {
            result = getProxyBuilder().doInvokeMethod(methodName, name, args);
        } catch (RuntimeException e) {
            // remove contexts created after we started
            if (getContexts().contains(previousContext)) {
                Map<String, Object> context = getProxyBuilder().getContext();
                while (context != null && context != previousContext) {
                    getProxyBuilder().popContext();
                    context = getProxyBuilder().getContext();
                }
            }
            throw e;
        }
        return result;
    }

    /**
     * Add an attribute delegate so it can intercept attributes being set.
     * Attribute delegates are fired in a FILO pattern, so that nested delegates
     * get first crack.
     *
     * @param attrDelegate the closure to be called
     * @return attrDelegate
     */
    public Closure addAttributeDelegate(Closure attrDelegate) {
        getProxyBuilder().attributeDelegates.addFirst(attrDelegate);
        return attrDelegate;
    }

    /**
     * Remove the most recently added instance of the attribute delegate.
     *
     * @param attrDelegate the instance of the closure to be removed
     */
    public void removeAttributeDelegate(Closure attrDelegate) {
        getProxyBuilder().attributeDelegates.remove(attrDelegate);
    }

    /**
     * Add a preInstantiate delegate so it can intercept nodes before they are
     * created. PreInstantiate delegates are fired in a FILO pattern, so that
     * nested delegates get first crack.
     *
     * @param delegate the closure to invoke
     * @return delegate
     */
    public Closure addPreInstantiateDelegate(Closure delegate) {
        getProxyBuilder().preInstantiateDelegates.addFirst(delegate);
        return delegate;
    }

    /**
     * Remove the most recently added instance of the preInstantiate delegate.
     *
     * @param delegate the closure to invoke
     */
    public void removePreInstantiateDelegate(Closure delegate) {
        getProxyBuilder().preInstantiateDelegates.remove(delegate);
    }

    /**
     * Add a postInstantiate delegate so it can intercept nodes after they are
     * created. PostInstantiate delegates are fired in a FILO pattern, so that
     * nested delegates get first crack.
     *
     * @param delegate the closure to invoke
     * @return delegate
     */
    public Closure addPostInstantiateDelegate(Closure delegate) {
        getProxyBuilder().postInstantiateDelegates.addFirst(delegate);
        return delegate;
    }

    /**
     * Remove the most recently added instance of the postInstantiate delegate.
     *
     * @param delegate the closure to invoke
     */
    public void removePostInstantiateDelegate(Closure delegate) {
        getProxyBuilder().postInstantiateDelegates.remove(delegate);
    }

    /**
     * Add a nodeCompletion delegate so it can intercept nodes after they done
     * with building. NodeCompletion delegates are fired in a FILO pattern, so
     * that nested delegates get first crack.
     *
     * @param delegate the closure to invoke
     * @return delegate
     */
    public Closure addPostNodeCompletionDelegate(Closure delegate) {
        getProxyBuilder().postNodeCompletionDelegates.addFirst(delegate);
        return delegate;
    }

    /**
     * Remove the most recently added instance of the nodeCompletion delegate.
     *
     * @param delegate the closure to be removed
     */
    public void removePostNodeCompletionDelegate(Closure delegate) {
        getProxyBuilder().postNodeCompletionDelegates.remove(delegate);
    }

    public void registerExplicitProperty(String name, Closure getter, Closure setter) {
        registerExplicitProperty(name, registrationGroupName, getter, setter);
    }

    public void registerExplicitProperty(String name, String groupName, Closure getter, Closure setter) {
        // set the delegate to FBS so the closure closes over the builder
        if (getter != null) getter.setDelegate(this);
        if (setter != null) setter.setDelegate(this);
        explicitProperties.put(name, new Closure[]{getter, setter});
        String methodNameBase = capitalize(name);
        if (getter != null) {
            getRegistrationGroup(groupName).add("get" + methodNameBase);
        }
        if (setter != null) {
            getRegistrationGroup(groupName).add("set" + methodNameBase);
        }
    }

    public void registerExplicitMethod(String name, Closure closure) {
        registerExplicitMethod(name, registrationGroupName, closure);
    }

    public void registerExplicitMethod(String name, String groupName, Closure closure) {
        // set the delegate to FBS so the closure closes over the builder
        closure.setDelegate(this);
        explicitMethods.put(name, closure);
        getRegistrationGroup(groupName).add(name);
    }

    /**
     * Registers a factory for a JavaBean.<br>
     * The JavaBean class should have a no-arg constructor.
     *
     * @param theName   name of the node
     * @param beanClass the factory to handle the name
     */
    public void registerBeanFactory(String theName, Class beanClass) {
        registerBeanFactory(theName, registrationGroupName, beanClass);
    }

    /**
     * Registers a factory for a JavaBean.<br>
     * The JavaBean class should have a no-arg constructor.
     *
     * @param theName   name of the node
     * @param groupName thr group to register this node in
     * @param beanClass the factory to handle the name
     */
    public void registerBeanFactory(String theName, String groupName, final Class beanClass) {
        getProxyBuilder().registerFactory(theName, new AbstractFactory() {
            @Override
            public Object newInstance(FactoryBuilderSupport builder, Object name, Object value,
                                      Map properties) throws InstantiationException, IllegalAccessException {
                if (checkValueIsTypeNotString(value, name, beanClass)) {
                    return value;
                } else {
                    try {
                        return beanClass.getDeclaredConstructor().newInstance();
                    } catch (NoSuchMethodException | InvocationTargetException e) {
                        throw new GroovyRuntimeException("Failed to register bean factory", e);
                    }
                }
            }
        });
        getRegistrationGroup(groupName).add(theName);
    }

    /**
     * Registers a factory for a node name.
     *
     * @param name    the name of the node
     * @param factory the factory to return the values
     */
    public void registerFactory(String name, Factory factory) {
        registerFactory(name, registrationGroupName, factory);
    }

    /**
     * Registers a factory for a node name.
     *
     * @param name      the name of the node
     * @param groupName thr group to register this node in
     * @param factory   the factory to return the values
     */
    public void registerFactory(String name, String groupName, Factory factory) {
        getProxyBuilder().factories.put(name, factory);
        getRegistrationGroup(groupName).add(name);
        factory.onFactoryRegistration(this, name, groupName);
    }

    /**
     * This method is responsible for instantiating a node and configure its
     * properties.
     *
     * @param name       the name of the node
     * @param attributes the attributes for the node
     * @param value      the value arguments for the node
     * @return the object return from the factory
     */
    protected Object createNode(Object name, Map attributes, Object value) {
        Object node;

        Factory factory = getProxyBuilder().resolveFactory(name, attributes, value);
        if (factory == null) {
            LOG.log(Level.WARNING, "Could not find match for name '" + name + "'");
            throw new MissingMethodExceptionNoStack((String) name, Object.class, new Object[]{attributes, value});
            //return null;
        }
        getProxyBuilder().getContext().put(CURRENT_FACTORY, factory);
        getProxyBuilder().getContext().put(CURRENT_NAME, String.valueOf(name));
        getProxyBuilder().preInstantiate(name, attributes, value);
        try {
            node = factory.newInstance(getProxyBuilder().getChildBuilder(), name, value, attributes);
            if (node == null) {
                LOG.log(Level.WARNING, "Factory for name '" + name + "' returned null");
                return null;
            }

            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("For name: " + name + " created node: " + node);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to create component for '" + name + "' reason: "
                    + e, e);
        }
        getProxyBuilder().postInstantiate(name, attributes, node);
        getProxyBuilder().handleNodeAttributes(node, attributes);
        return node;
    }

    /**
     * This is a hook for subclasses to plug in a custom strategy for mapping
     * names to factories.
     *
     * @param name       the name of the factory
     * @param attributes the attributes from the node
     * @param value      value arguments from te node
     * @return the Factory associated with name.<br>
     */
    protected Factory resolveFactory(Object name, Map attributes, Object value) {
        getProxyBuilder().getContext().put(CHILD_BUILDER, getProxyBuilder());
        return getProxyBuilder().getFactories().get(name);
    }

    /**
     * This is a hook for subclasses to plug in a custom strategy for mapping
     * names to explicit methods.
     *
     * @param methodName the name of the explicit method
     * @param args       the arguments for the method
     * @return the closure for the matched explicit method.<br>
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected Closure resolveExplicitMethod(String methodName, Object args) {
        return getExplicitMethods().get(methodName);
    }

    /**
     * This is a hook for subclasses to plug in a custom strategy for mapping
     * names to property methods.
     *
     * @param propertyName the name of the explicit method
     * @return the get and set closures (in that order) for the matched explicit property.<br>
     */
    protected Closure[] resolveExplicitProperty(String propertyName) {
        return getExplicitProperties().get(propertyName);
    }

    /**
     * This method is the workhorse of the builder.
     *
     * @param methodName the name of the method being invoked
     * @param name       the name of the node
     * @param args       the arguments passed into the node
     * @return the object from the factory
     */
    private Object doInvokeMethod(String methodName, Object name, Object args) {
        Reference explicitResult = new Reference();
        if (checkExplicitMethod(methodName, args, explicitResult)) {
            return explicitResult.get();
        } else {
            try {
                return dispatchNodeCall(name, args);
            } catch(MissingMethodException mme) {
                if(mme.getMethod().equals(methodName) && methodMissingDelegate != null) {
                    return methodMissingDelegate.call(methodName, args);
                }
                throw mme;
            }
        }
    }

    protected boolean checkExplicitMethod(String methodName, Object args, Reference result) {
        Closure explicitMethod = resolveExplicitMethod(methodName, args);
        if (explicitMethod != null) {
            if (args instanceof Object[]) {
                result.set(explicitMethod.call((Object[]) args));
            } else {
                //todo push through InvokerHelper.asList?
                result.set(explicitMethod.call(args));
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Use {@link FactoryBuilderSupport#dispatchNodeCall(Object, Object)} instead.
     */
    @Deprecated
    protected Object dispathNodeCall(Object name, Object args) {
        return dispatchNodeCall(name, args);
    }

    protected Object dispatchNodeCall(Object name, Object args) {
        Object node;
        Closure closure = null;
        List list = InvokerHelper.asList(args);

        final boolean needToPopContext;
        if (getProxyBuilder().getContexts().isEmpty()) {
            // should be called on first build method only
            getProxyBuilder().newContext();
            needToPopContext = true;
        } else {
            needToPopContext = false;
        }

        try {
            Map namedArgs = Collections.EMPTY_MAP;

            // the arguments come in like [named_args?, args..., closure?]
            // so peel off a hashmap from the front, and a closure from the
            // end and presume that is what they meant, since there is
            // no way to distinguish node(a:b,c,d) {..} from
            // node([a:b],[c,d], {..}), i.e. the user can deliberately confuse
            // the builder and there is nothing we can really do to prevent
            // that

            if ((!list.isEmpty())
                    && (list.get(0) instanceof LinkedHashMap)) {
                namedArgs = (Map) list.get(0);
                list = list.subList(1, list.size());
            }
            if ((!list.isEmpty())
                    && (list.get(list.size() - 1) instanceof Closure)) {
                closure = (Closure) list.get(list.size() - 1);
                list = list.subList(0, list.size() - 1);
            }
            Object arg;
            if (list.isEmpty()) {
                arg = null;
            } else if (list.size() == 1) {
                arg = list.get(0);
            } else {
                arg = list;
            }
            node = getProxyBuilder().createNode(name, namedArgs, arg);

            Object current = getProxyBuilder().getCurrent();
            if (current != null) {
                getProxyBuilder().setParent(current, node);
            }

            if (closure != null) {
                Factory parentFactory = getProxyBuilder().getCurrentFactory();
                if (parentFactory.isLeaf()) {
                    throw new RuntimeException("'" + name + "' doesn't support nesting.");
                }
                boolean processContent = true;
                if (parentFactory.isHandlesNodeChildren()) {
                    processContent = parentFactory.onNodeChildren(this, node, closure);
                }
                if (processContent) {
                    // push new node on stack
                    String parentName = getProxyBuilder().getCurrentName();
                    Map parentContext = getProxyBuilder().getContext();
                    getProxyBuilder().newContext();
                    try {
                        getProxyBuilder().getContext().put(OWNER, closure.getOwner());
                        getProxyBuilder().getContext().put(CURRENT_NODE, node);
                        getProxyBuilder().getContext().put(PARENT_FACTORY, parentFactory);
                        getProxyBuilder().getContext().put(PARENT_NODE, current);
                        getProxyBuilder().getContext().put(PARENT_CONTEXT, parentContext);
                        getProxyBuilder().getContext().put(PARENT_NAME, parentName);
                        getProxyBuilder().getContext().put(PARENT_BUILDER, parentContext.get(CURRENT_BUILDER));
                        getProxyBuilder().getContext().put(CURRENT_BUILDER, parentContext.get(CHILD_BUILDER));
                        // let's register the builder as the delegate
                        getProxyBuilder().setClosureDelegate(closure, node);
                        closure.call();
                    } finally {
                        getProxyBuilder().popContext();
                    }
                }
            }

            getProxyBuilder().nodeCompleted(current, node);
            node = getProxyBuilder().postNodeCompletion(current, node);
        } finally {
            if (needToPopContext) {
                // pop the first context
                getProxyBuilder().popContext();
            }
        }
        return node;
    }

    /**
     * A hook to allow names to be converted into some other object such as a
     * QName in XML or ObjectName in JMX.
     *
     * @param methodName the name of the desired method
     * @return the object representing the name
     */
    public Object getName(String methodName) {
        if (getProxyBuilder().nameMappingClosure != null) {
            return getProxyBuilder().nameMappingClosure.call(methodName);
        }
        return methodName;
    }

    /**
     * Proxy builders are useful for changing the building context, thus
     * enabling mix &amp; match builders.
     *
     * @return the current builder that serves as a proxy.<br>
     */
    protected FactoryBuilderSupport getProxyBuilder() {
        FactoryBuilderSupport proxy = localProxyBuilder.get();
        if (proxy == null) {
            return globalProxyBuilder;
        } else {
            return proxy;
        }
    }

    /**
     * Sets the builder to be used as a proxy.
     *
     * @param proxyBuilder the new proxy
     */
    protected void setProxyBuilder(FactoryBuilderSupport proxyBuilder) {
        globalProxyBuilder = proxyBuilder;
    }

    public Closure getNameMappingClosure() {
        return nameMappingClosure;
    }

    public void setNameMappingClosure(Closure nameMappingClosure) {
        this.nameMappingClosure = nameMappingClosure;
    }

    /**
     * Assigns any existing properties to the node.<br>
     * It will call attributeDelegates before passing control to the factory
     * that built the node.
     *
     * @param node       the object returned by tne node factory
     * @param attributes the attributes for the node
     */
    protected void handleNodeAttributes(Object node, Map attributes) {
        // first, short circuit
        if (node == null) {
            return;
        }

        for (Closure attrDelegate : getProxyBuilder().getAttributeDelegates()) {
            FactoryBuilderSupport builder = this;
            if (attrDelegate.getOwner() instanceof FactoryBuilderSupport) {
                builder = (FactoryBuilderSupport) attrDelegate.getOwner();
            } else if (attrDelegate.getDelegate() instanceof FactoryBuilderSupport) {
                builder = (FactoryBuilderSupport) attrDelegate.getDelegate();
            }

            attrDelegate.call(builder, node, attributes);
        }

        if (getProxyBuilder().getCurrentFactory().onHandleNodeAttributes(getProxyBuilder().getChildBuilder(), node, attributes)) {
            getProxyBuilder().setNodeAttributes(node, attributes);
        }
    }

    /**
     * Pushes a new context on the stack.
     */
    protected void newContext() {
        getContexts().addFirst(new HashMap<String, Object>());
    }

    /**
     * A hook to allow nodes to be processed once they have had all of their
     * children applied.
     *
     * @param node   the current node being processed
     * @param parent the parent of the node being processed
     */
    protected void nodeCompleted(Object parent, Object node) {
        getProxyBuilder().getCurrentFactory().onNodeCompleted(getProxyBuilder().getChildBuilder(), parent, node);
    }

    /**
     * Removes the last context from the stack.
     *
     * @return the content just removed
     */
    protected Map<String, Object> popContext() {
        if (!getProxyBuilder().getContexts().isEmpty()) {
            return getProxyBuilder().getContexts().removeFirst();
        }
        return null;
    }

    /**
     * A hook after the factory creates the node and before attributes are set.<br>
     * It will call any registered postInstantiateDelegates, if you override
     * this method be sure to call this impl somewhere in your code.
     *
     * @param name       the name of the node
     * @param attributes the attributes for the node
     * @param node       the object created by the node factory
     */
    protected void postInstantiate(Object name, Map attributes, Object node) {
        for (Closure postInstantiateDelegate : getProxyBuilder().getPostInstantiateDelegates()) {
            (postInstantiateDelegate).call(this, attributes, node);
        }
    }

    /**
     * A hook to allow nodes to be processed once they have had all of their
     * children applied and allows the actual node object that represents the
     * Markup element to be changed.<br>
     * It will call any registered postNodeCompletionDelegates, if you override
     * this method be sure to call this impl at the end of your code.
     *
     * @param node   the current node being processed
     * @param parent the parent of the node being processed
     * @return the node, possibly new, that represents the markup element
     */
    protected Object postNodeCompletion(Object parent, Object node) {
        for (Closure postNodeCompletionDelegate : getProxyBuilder().getPostNodeCompletionDelegates()) {
            (postNodeCompletionDelegate).call(this, parent, node);
        }

        return node;
    }

    /**
     * A hook before the factory creates the node.<br>
     * It will call any registered preInstantiateDelegates, if you override this
     * method be sure to call this impl somewhere in your code.
     *
     * @param name       the name of the node
     * @param attributes the attributes of the node
     * @param value      the value argument(s) of the node
     */
    protected void preInstantiate(Object name, Map attributes, Object value) {
        for (Closure preInstantiateDelegate : getProxyBuilder().getPreInstantiateDelegates()) {
            (preInstantiateDelegate).call(this, attributes, value);
        }
    }

    /**
     * Clears the context stack.
     */
    protected void reset() {
        getProxyBuilder().getContexts().clear();
    }

    /**
     * A strategy method to allow derived builders to use builder-trees and
     * switch in different kinds of builders. This method should call the
     * setDelegate() method on the closure which by default passes in this but
     * if node is-a builder we could pass that in instead (or do something wacky
     * too)
     *
     * @param closure the closure on which to call setDelegate()
     * @param node    the node value that we've just created, which could be a
     *                builder
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected void setClosureDelegate(Closure closure, Object node) {
        closure.setDelegate(this);
    }

    /**
     * Maps attributes key/values to properties on node.
     *
     * @param node       the object from the node
     * @param attributes the attributes to be set
     */
    protected void setNodeAttributes(Object node, Map attributes) {
        // set the properties
        //noinspection unchecked
        for (Map.Entry entry : (Set<Map.Entry>) attributes.entrySet()) {
            String property = entry.getKey().toString();
            Object value = entry.getValue();
            InvokerHelper.setProperty(node, property, value);
        }
    }

    /**
     * Strategy method to establish parent/child relationships.
     *
     * @param parent the object from the parent node
     * @param child  the object from the child node
     */
    protected void setParent(Object parent, Object child) {
        getProxyBuilder().getCurrentFactory().setParent(getProxyBuilder().getChildBuilder(), parent, child);
        Factory parentFactory = getProxyBuilder().getParentFactory();
        if (parentFactory != null) {
            parentFactory.setChild(getProxyBuilder().getCurrentBuilder(), parent, child);
        }
    }

    /**
     * @return the stack of available contexts.
     */
    protected LinkedList<Map<String, Object>> getContexts() {
        LinkedList<Map<String, Object>> contexts = getProxyBuilder().contexts.get();
        if (contexts == null) {
            contexts = new LinkedList<Map<String, Object>>();
            getProxyBuilder().contexts.set(contexts);
        }
        return contexts;
    }

    /**
     * Stores the thread local states in a Map that can be passed across threads
     * @return the map
     */
    protected Map<String, Object> getContinuationData() {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("proxyBuilder", localProxyBuilder.get());
        data.put("contexts", contexts.get());
        return data;
    }

    /**
     * Restores the state of the current builder to the same state as an older build.
     *
     * Caution, this will destroy rather than merge the current build context if there is any,
     * @param data the data retrieved from a compatible getContinuationData call
     */
    protected void restoreFromContinuationData(Map<String, Object> data) {
        //noinspection unchecked
        localProxyBuilder.set((FactoryBuilderSupport) data.get("proxyBuilder"));
        //noinspection unchecked
        contexts.set((LinkedList<Map<String, Object>>) data.get("contexts"));
    }

    public Object build(Class viewClass) {
        if (Script.class.isAssignableFrom(viewClass)) {
            Script script = InvokerHelper.createScript(viewClass, this);
            return build(script);
        } else {
            throw new RuntimeException("Only scripts can be executed via build(Class)");
        }
    }

    public Object build(Script script) {
        // this used to be synchronized, but we also used to remove the
        // metaclass.  Since adding the metaclass is now a side effect, we
        // don't need to ensure the metaclass won't be observed and don't
        // need to hide the side effect.
        MetaClass scriptMetaClass = script.getMetaClass();
        script.setMetaClass(new FactoryInterceptorMetaClass(scriptMetaClass, this));
        script.setBinding(this);
        Object oldScriptName = getProxyBuilder().getVariables().get(SCRIPT_CLASS_NAME);
        try {
            getProxyBuilder().setVariable(SCRIPT_CLASS_NAME, script.getClass().getName());
            return script.run();
        } finally {
            if(oldScriptName != null) {
                getProxyBuilder().setVariable(SCRIPT_CLASS_NAME, oldScriptName);
            } else {
                getProxyBuilder().getVariables().remove(SCRIPT_CLASS_NAME);
            }
        }
    }

    public Object build(final String script, GroovyClassLoader loader) {
        return build(loader.parseClass(script));
    }

    /**
     * Switches the builder's proxyBuilder during the execution of a closure.<br>
     * This is useful to temporary change the building context to another builder
     * without the need for a contrived setup. It will also take care of restoring
     * the previous proxyBuilder when the execution finishes, even if an exception
     * was thrown from inside the closure.
     *
     * @param builder the temporary builder to switch to as proxyBuilder.
     * @param closure the closure to be executed under the temporary builder.
     * @return the execution result of the closure.
     * @throws RuntimeException - any exception the closure might have thrown during
     *                          execution.
     */
    public Object withBuilder(FactoryBuilderSupport builder, Closure closure) {
        if (builder == null || closure == null) {
            return null;
        }

        Object result = null;
        Object previousContext = getProxyBuilder().getContext();
        FactoryBuilderSupport previousProxyBuilder = localProxyBuilder.get();
        try {
            localProxyBuilder.set(builder);
            closure.setDelegate(builder);
            result = closure.call();
        }
        catch (RuntimeException e) {
            // remove contexts created after we started
            localProxyBuilder.set(previousProxyBuilder);
            if (getProxyBuilder().getContexts().contains(previousContext)) {
                Map<String, Object> context = getProxyBuilder().getContext();
                while (context != null && context != previousContext) {
                    getProxyBuilder().popContext();
                    context = getProxyBuilder().getContext();
                }
            }
            throw e;
        }
        finally {
            localProxyBuilder.set(previousProxyBuilder);
        }

        return result;
    }

    /**
     * Switches the builder's proxyBuilder during the execution of a closure.<br>
     * This is useful to temporary change the building context to another builder
     * without the need for a contrived setup. It will also take care of restoring
     * the previous proxyBuilder when the execution finishes, even if an exception
     * was thrown from inside the closure. Additionally it will use the closure's
     * result as the value for the node identified by 'name'.
     *
     * @param builder the temporary builder to switch to as proxyBuilder.
     * @param name    the node to build on the 'parent' builder.
     * @param closure the closure to be executed under the temporary builder.
     * @return a node that responds to value of name with the closure's result as its
     *         value.
     * @throws RuntimeException - any exception the closure might have thrown during
     *                          execution.
     */
    public Object withBuilder(FactoryBuilderSupport builder, String name, Closure closure) {
        if (name == null) {
            return null;
        }
        Object result = getProxyBuilder().withBuilder(builder, closure);
        return getProxyBuilder().invokeMethod(name, new Object[]{result});
    }

    /**
     * Switches the builder's proxyBuilder during the execution of a closure.<br>
     * This is useful to temporary change the building context to another builder
     * without the need for a contrived setup. It will also take care of restoring
     * the previous proxyBuilder when the execution finishes, even if an exception
     * was thrown from inside the closure. Additionally it will use the closure's
     * result as the value for the node identified by 'name' and assign any attributes
     * that might have been set.
     *
     * @param attributes additional properties for the node on the parent builder.
     * @param builder    the temporary builder to switch to as proxyBuilder.
     * @param name       the node to build on the 'parent' builder.
     * @param closure    the closure to be executed under the temporary builder.
     * @return a node that responds to value of name with the closure's result as its
     *         value.
     * @throws RuntimeException - any exception the closure might have thrown during
     *                          execution.
     */
    public Object withBuilder(Map attributes, FactoryBuilderSupport builder, String name, Closure closure) {
        if (name == null) {
            return null;
        }
        Object result = getProxyBuilder().withBuilder(builder, closure);
        return getProxyBuilder().invokeMethod(name, new Object[]{attributes, result});
    }

    public void addDisposalClosure(Closure closure) {
        disposalClosures.add(closure);
    }

    public List<Closure> getDisposalClosures() {
        return Collections.unmodifiableList(disposalClosures);
    }

    public void dispose() {
        for (int i = disposalClosures.size() - 1; i >= 0; i--) {
            disposalClosures.get(i).call();
        }
    }
}

class FactoryInterceptorMetaClass extends DelegatingMetaClass {

    FactoryBuilderSupport builder;

    FactoryInterceptorMetaClass(MetaClass delegate, FactoryBuilderSupport builder) {
        super(delegate);
        this.builder = builder;
    }

    @Override
    public Object invokeMethod(Object object, String methodName, Object arguments) {
        try {
            return delegate.invokeMethod(object, methodName, arguments);
        } catch (MissingMethodException mme) {
            // attempt builder resolution
            try {
                if (builder.getMetaClass().respondsTo(builder, methodName).isEmpty()) {
                    // dispatch to factories if it is not a literal method
                    return builder.invokeMethod(methodName, arguments);
                } else {
                    return InvokerHelper.invokeMethod(builder, methodName, arguments);
                }
            } catch (MissingMethodException mme2) {
                // chain secondary exception
                Throwable root = mme;
                while (root.getCause() != null) {
                    root = root.getCause();
                }
                root.initCause(mme2);
                // throw original
                throw mme;
            }
        }
    }

    @Override
    public Object invokeMethod(Object object, String methodName, Object[] arguments) {
        try {
            return delegate.invokeMethod(object, methodName, arguments);
        } catch (MissingMethodException mme) {
            // attempt builder resolution
            try {
                if (builder.getMetaClass().respondsTo(builder, methodName).isEmpty()) {
                    // dispatch to factories if it is not a literal method
                    return builder.invokeMethod(methodName, arguments);
                } else {
                    return InvokerHelper.invokeMethod(builder, methodName, arguments);
                }
            } catch (MissingMethodException mme2) {
                // chain secondary exception
                Throwable root = mme;
                while (root.getCause() != null) {
                    root = root.getCause();
                }
                root.initCause(mme2);
                // throw original
                throw mme;
            }
        }
    }
}
