/*
 * Copyright 2002-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.config.java.internal.enhancement;

import static java.lang.String.format;
import static org.springframework.config.java.internal.factory.BeanVisibility.visibilityOf;
import static org.springframework.config.java.util.DefaultScopes.SINGLETON;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.springframework.util.Assert.notNull;
import static org.springframework.util.ClassUtils.getDefaultClassLoader;
import static org.springframework.util.StringUtils.hasLength;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import net.sf.cglib.core.DefaultGeneratorStrategy;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.asm.ClassAdapter;
import org.springframework.asm.ClassReader;
import org.springframework.asm.ClassWriter;
import org.springframework.beans.BeanMetadataAttribute;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.config.java.annotation.AutoBean;
import org.springframework.config.java.annotation.Bean;
import org.springframework.config.java.annotation.ExternalBean;
import org.springframework.config.java.annotation.ExternalValue;
import org.springframework.config.java.annotation.aop.ScopedProxy;
import org.springframework.config.java.internal.factory.BeanVisibility;
import org.springframework.config.java.internal.factory.JavaConfigBeanFactory;
import org.springframework.config.java.internal.factory.support.ConfigurationModelBeanDefinitionReader;
import org.springframework.config.java.internal.util.Constants;
import org.springframework.config.java.model.ModelMethod;
import org.springframework.config.java.valuesource.CompositeValueResolver;
import org.springframework.config.java.valuesource.ValueResolutionException;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;


/** TODO: JAVADOC */
public class CglibConfigurationEnhancer implements ConfigurationEnhancer {

    private static final Log log = LogFactory.getLog(CglibConfigurationEnhancer.class);

    private static final CallbackFilter CALLBACK_FILTER =
        new CallbackFilter() {
            public int accept(Method candidateMethod) {
                if (findAnnotation(candidateMethod, Bean.class) != null)
                    return 1;

                if (findAnnotation(candidateMethod, ExternalBean.class) != null)
                    return 2;

                if (findAnnotation(candidateMethod, AutoBean.class) != null)
                    return 3;

                if (findAnnotation(candidateMethod, ExternalValue.class) != null)
                    return 4;

                return 0;
            }
        };

    private static final Class<?>[] CALLBACK_TYPES =
        new Class<?>[] {
            NoOp.class,
            BeanMethodInterceptor.class,
            ExternalBeanMethodInterceptor.class,
            AutoBeanMethodInterceptor.class,
            ExternalValueMethodInterceptor.class
        };

    private final JavaConfigBeanFactory beanFactory;


    public CglibConfigurationEnhancer(JavaConfigBeanFactory beanFactory) {
        notNull(beanFactory, "beanFactory must be non-null");
        this.beanFactory = beanFactory;

        registerThreadLocalCleanupBeanDefinition();
    }

    private void registerThreadLocalCleanupBeanDefinition() {
        AbstractBeanDefinition beanDef = new RootBeanDefinition(CglibCallbackThreadLocalCleanup.class);
        beanDef.addMetadataAttribute(new BeanMetadataAttribute(Constants.JAVA_CONFIG_IGNORE, true));
        beanFactory.registerBeanDefinition("cglibThreadLocalCleanup", beanDef);
    }

    public String enhance(String configClassName) {
        if (log.isInfoEnabled())
            log.info("Enhancing " + configClassName);

        Class<?> superclass = loadClassFromName(configClassName);

        Class<?> subclass = newEnhancer(superclass).createClass();
        
        subclass = nestOneClassDeeperIfAspect(superclass, subclass);
        
        // This call populates ThreadLocal state that must get cleaned
        // up to avoid a memory leak.  The typical way this gets done
        // is calling
        //     Enhancer.registerCallbacks(enhancedSubclass, null);
        // but this doesn't work in our case as the thread that would make
        // that call may not always be the same thread that registered the
        // state.
        // when the container goes down *only* if close() is explicitly
        // called or registerShutdownHook() has been called.
        // see CglibThreadLocalStateManager
        Enhancer.registerCallbacks(subclass,
                                   new Callback[] {
                                       NoOp.INSTANCE,
                                       new BeanMethodInterceptor(beanFactory),
                                       new ExternalBeanMethodInterceptor(beanFactory),
                                       new AutoBeanMethodInterceptor(beanFactory),
                                       new ExternalValueMethodInterceptor(beanFactory)
                                   });

        if (log.isInfoEnabled())
            log.info(format("Successfully enhanced %s; enhanced class name is: %s",
                            configClassName, subclass.getName()));

        return subclass.getName();
    }

    private Class<?> nestOneClassDeeperIfAspect(Class<?> superclass, Class<?> subclass) {
        boolean superclassIsAnAspect = false;
        
        // check for @Aspect by name rather than by class literal to avoid
        // requiring AspectJ as a runtime dependency.
        for(Annotation anno : superclass.getAnnotations())
            if(anno.annotationType().getName().equals("org.aspectj.lang.annotation.Aspect"))
                superclassIsAnAspect = true;
        
        if(!superclassIsAnAspect)
	        return subclass;
        
        // the superclass is annotated with AspectJ's @Aspect.
        // this means that we must create a subclass of the subclass
        // in order to avoid some guard logic in Spring core that disallows
        // extending a concrete aspect class.
        Enhancer enhancer = newEnhancer(subclass);
        enhancer.setStrategy(new DefaultGeneratorStrategy() {
            @Override
            protected byte[] transform(byte[] b) throws Exception {
                ClassWriter writer = new ClassWriter(false);
                // TODO: create util for going from class -> type descriptor
                ClassAdapter adapter = new AddAnnotationAdapter(writer, "Lorg/aspectj/lang/annotation/Aspect;");
                ClassReader reader = new ClassReader(b);
                reader.accept(adapter, false);
                return writer.toByteArray();
            }
        });
        
        // create a subclass of the original subclass
        return enhancer.createClass();
    }

    private static Enhancer newEnhancer(Class<?> superclass) {
        Enhancer enhancer = new Enhancer();

        // true is the default when it comes to the cache, but
        // calling it out explicitly here to emphasize the contract
        // that all properties on this enhancer instance must be the
        // same in order to get caching to work.  i.e.: CALLBACK_FILTER
        // and CALLBACK_TYPES *must* be the same instances (thus their
        // being static final)
        enhancer.setUseCache(true);
        enhancer.setSuperclass(superclass);
        enhancer.setUseFactory(false);
        enhancer.setCallbackFilter(CALLBACK_FILTER);
        enhancer.setCallbackTypes(CALLBACK_TYPES);
        
        return enhancer;
    }

    private Class<?> loadClassFromName(String configClassName) {
        try {
            return getDefaultClassLoader().loadClass(configClassName);
        } catch (ClassNotFoundException ex) {
            throw new IllegalArgumentException("class must be loadable", ex);
        }
    }

    abstract static class AbstractMethodInterceptor implements MethodInterceptor {
        protected final Log log = LogFactory.getLog(this.getClass());
        protected final JavaConfigBeanFactory beanFactory;

        public AbstractMethodInterceptor(JavaConfigBeanFactory beanFactory) { this.beanFactory = beanFactory; }

        public Object intercept(Object o, Method m, Object[] args, MethodProxy mp) throws Throwable {
            ModelMethod beanMethod = ModelMethod.forMethod(m);
            String beanName = beanFactory.getBeanNamingStrategy().getBeanName(beanMethod);
            return doIntercept(o, m, args, mp, beanName);
        }

        protected abstract Object doIntercept(Object o, Method m, Object[] args, MethodProxy mp, String beanName)
                                       throws Throwable;
    }

    static class ExternalBeanMethodInterceptor extends AbstractMethodInterceptor {

        public ExternalBeanMethodInterceptor(JavaConfigBeanFactory beanFactory) { super(beanFactory); }

        @Override
        public Object doIntercept(Object o, Method m, Object[] args, MethodProxy mp, String beanName) throws Throwable {
            ExternalBean extBean = AnnotationUtils.findAnnotation(m, ExternalBean.class);
            Assert.notNull(extBean, "ExternalBean methods must be annotated with @ExternalBean");

            String alternateName = extBean.value();

            return beanFactory.getBean(hasLength(alternateName) ? alternateName : beanName);
        }
    }

    static class ExternalValueMethodInterceptor extends AbstractMethodInterceptor {
        private CompositeValueResolver valueResolver;

        public ExternalValueMethodInterceptor(JavaConfigBeanFactory beanFactory) { super(beanFactory); }

        @Override
        public Object doIntercept(Object o, Method m, Object[] args, MethodProxy mp, String beanName) throws Throwable {
            ExternalValue metadata = AnnotationUtils.findAnnotation(m, ExternalValue.class);
            Assert.notNull(metadata, "ExternalValue methods must be annotated with @ExternalValue");

            String name = metadata.value();
            if (!hasLength(name)) {
                // no explicit name provided -> use method name
                name = m.getName();
                // Strip property name if needed
                if (name.startsWith("get") && Character.isUpperCase(name.charAt(3)))
                    name = Character.toLowerCase(name.charAt(3)) + name.substring(4);
            }

            Class<?> requiredType = m.getReturnType();

            if (valueResolver == null)
                valueResolver = CompositeValueResolver.forMember(beanFactory.getParentBeanFactory(), m);

            try {
                return valueResolver.resolve(name, requiredType);
            } catch (ValueResolutionException ex) {
                // value was not found in properties -> default to the body of the method (if any
                // exists)
                if (Modifier.isAbstract(m.getModifiers()))
                    throw ex; // cannot call super implementation if it's abstract.

                return mp.invokeSuper(o, args);
            }
        }

    }

    static class AutoBeanMethodInterceptor extends AbstractMethodInterceptor {

        public AutoBeanMethodInterceptor(JavaConfigBeanFactory beanFactory) { super(beanFactory); }

        @Override
        public Object doIntercept(Object o, Method m, Object[] args, MethodProxy mp, String beanName) throws Throwable {
            AutoBean metadata = AnnotationUtils.findAnnotation(m, AutoBean.class);
            Assert.notNull(metadata, "AutoBean methods must be annotated with @AutoBean");

            return beanFactory.getBean(beanName);
        }
    }

    /**
     * Intercepts calls to {@link Bean @Bean} methods delegating to
     * {@link #intercept(Object, Method, Object[], MethodProxy) intercept()} in order to ensure
     * proper bean functionality: singleton, AOP proxying, etc.
     *
     * @author  Chris Beams
     */
    static class BeanMethodInterceptor extends AbstractMethodInterceptor {

        public BeanMethodInterceptor(JavaConfigBeanFactory beanFactory) {
            super(beanFactory);
        }

        /**
         * Enhances a {@link Bean @Bean} method to check the supplied BeanFactory for the existence
         * of this bean object.
         */
        @Override
        public Object doIntercept(Object o, Method m, Object[] args, MethodProxy mp, String _beanName)
                           throws Throwable {
            boolean isScopedProxy = (AnnotationUtils.findAnnotation(m, ScopedProxy.class) != null);

            final String beanName;
            String scopedBeanName = ConfigurationModelBeanDefinitionReader.resolveHiddenScopedProxyBeanName(_beanName);
            if (isScopedProxy && beanFactory.isCurrentlyInCreation(scopedBeanName))
                beanName = scopedBeanName;
            else
                beanName = _beanName;

            if (factoryContainsBean(beanName)) {
                // we have an already existing cached instance of this bean -> retrieve it
                Object cachedBean = beanFactory.getBean(beanName);
                if (log.isInfoEnabled())
                    log.info(format("Returning cached singleton object [%s] for @Bean method %s.%s",
                                    cachedBean, m.getDeclaringClass().getSimpleName(), beanName));

                return cachedBean;
            }

            // no instance exists yet -> create a new one
            Object bean = mp.invokeSuper(o, args);

            Bean metadata = AnnotationUtils.findAnnotation(m, Bean.class);

            if (metadata.scope().equals(SINGLETON)) {
                BeanVisibility visibility = visibilityOf(m.getModifiers());
                if (log.isInfoEnabled())
                    log.info(format("Registering new %s singleton object [%s] for @Bean method %s.%s",
                                    visibility, bean, m.getDeclaringClass().getSimpleName(), beanName));

                beanFactory.registerSingleton(beanName, bean, visibility);
            }

            return bean;
        }

        /**
         * Check the beanFactory to see whether the bean named <var>beanName</var> already exists.
         * Accounts for the fact that the requested bean may be "in creation", i.e.: we're in the
         * middle of servicing the initial request for this bean. From JavaConfig's perspective,
         * this means that the bean does not actually yet exist, and that it is now our job to
         * create it for the first time by executing the logic in the corresponding Bean method.
         *
         * @param   beanName  name of bean to check for
         *
         * @return  true if <var>beanName</var> already exists in beanFactory
         */
        private boolean factoryContainsBean(String beanName) {
            return beanFactory.containsBean(beanName)
                       && !beanFactory.isCurrentlyInCreation(beanName);
        }

    }

}
