/*
 * 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.parsing.asm;

import static org.springframework.asm.Opcodes.ACC_INTERFACE;
import static org.springframework.config.java.internal.parsing.asm.AsmUtils.newClassReader;
import static org.springframework.config.java.internal.parsing.asm.MutableAnnotationUtils.createMutableAnnotation;
import static org.springframework.util.ClassUtils.convertClassNameToResourcePath;

import java.util.HashMap;

import org.springframework.asm.AnnotationVisitor;
import org.springframework.asm.ClassAdapter;
import org.springframework.asm.ClassReader;
import org.springframework.asm.Label;
import org.springframework.asm.MethodAdapter;
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.model.AutoBeanMethod;
import org.springframework.config.java.internal.model.BeanMethod;
import org.springframework.config.java.internal.model.ConfigurationClass;
import org.springframework.config.java.internal.model.ExternalBeanMethod;
import org.springframework.config.java.internal.model.ExternalValueMethod;
import org.springframework.config.java.internal.model.NonJavaConfigMethod;
import org.springframework.config.java.model.ModelClass;
import org.springframework.config.java.model.ModelMethod;


/** TODO: JAVADOC */
class ConfigurationClassMethodVisitor extends MethodAdapter {

    private final ConfigurationClass configClass;
    private final String methodName;
    private final int modifiers;
    private final ModelClass returnType;
    private final boolean isConstructor;
    private final HashMap<String, ScopedProxy> scopedProxiesByMethodName = new HashMap<String, ScopedProxy>();

    private boolean isJavaConfigMethod = false;
    private ModelMethod lastAddedMethod;
    private int lineNumber;

    public ConfigurationClassMethodVisitor(ConfigurationClass configClass, String methodName,
                                           String methodDescriptor, int modifiers) {
        super(AsmUtils.EMPTY_VISITOR);

        this.configClass = configClass;
        this.methodName = methodName;
        this.returnType = new ModelClass(AsmUtils.getReturnTypeFromMethodDescriptor(methodDescriptor));
        if(!"".equals(returnType.getName())) {
            String className = returnType.getName();
            ClassReader reader = newClassReader(convertClassNameToResourcePath(className));
            reader.accept(
                new ClassAdapter(AsmUtils.EMPTY_VISITOR) {
                    @Override
                    public void visit(int arg0, int arg1, String arg2, String arg3, String arg4, String[] arg5) {
                        returnType.setInterface((arg1 & ACC_INTERFACE) == ACC_INTERFACE);
                    }
                }, false);
        }
        this.modifiers = modifiers;

        this.isConstructor = methodName.equals("<init>");
    }

    @Override
    public AnnotationVisitor visitAnnotation(String annoTypeDesc, boolean arg1) {
        String annoTypeName = AsmUtils.convertTypeDescriptorToClassName(annoTypeDesc);

        // TODO: consider making this section a generic algorithm.  Look up mutable impls from registry, etc.
        if (Bean.class.getName().equals(annoTypeName)) {
            // we found a @Bean method, therefore it's a JavaConfig method
            isJavaConfigMethod = true;
            Bean mutableAnno = createMutableAnnotation(Bean.class);
            BeanMethod method = new BeanMethod(methodName, modifiers, mutableAnno);
            lastAddedMethod = method;
            configClass.add(method);
            return new MutableAnnotationVisitor((MutableAnnotation) mutableAnno);
        }

        if (ExternalBean.class.getName().equals(annoTypeName)) {
            // we found an @ExternalBean method, therefore it's a JavaConfig method
            isJavaConfigMethod = true;
            ExternalBean mutableAnno = createMutableAnnotation(ExternalBean.class);
            ExternalBeanMethod method = new ExternalBeanMethod(methodName, modifiers, mutableAnno);
            lastAddedMethod = method;
            configClass.add(method);
            return new MutableAnnotationVisitor((MutableAnnotation) mutableAnno);
        }

        if (ScopedProxy.class.getName().equals(annoTypeName)) {
            // note that the presence of @ScopedProxy does not necessarily indicate it's a JavaConfig method
            ScopedProxy mutableAnno = createMutableAnnotation(ScopedProxy.class);
            scopedProxiesByMethodName.put(methodName, mutableAnno);
            return new MutableAnnotationVisitor((MutableAnnotation) mutableAnno);
        }

        if (ExternalValue.class.getName().equals(annoTypeName)) {
            // we found an @ExternalValue method, therefore it's a JavaConfig method
            isJavaConfigMethod = true;
            ExternalValue mutableAnno = createMutableAnnotation(ExternalValue.class);
            ExternalValueMethod method = new ExternalValueMethod(methodName, modifiers, mutableAnno);
            lastAddedMethod = method;
            configClass.add(method);
            return new MutableAnnotationVisitor((MutableAnnotation) mutableAnno);
        }

        if (AutoBean.class.getName().equals(annoTypeName)) {
            // we found an @AutoBean method, therefore it's a JavaConfig method
            isJavaConfigMethod = true;
            AutoBean mutableAnno = createMutableAnnotation(AutoBean.class);
            AutoBeanMethod method = new AutoBeanMethod(methodName, returnType, modifiers, mutableAnno);
            lastAddedMethod = method;
            configClass.add(method);
            return new MutableAnnotationVisitor((MutableAnnotation) mutableAnno);
        }

        return super.visitAnnotation(annoTypeDesc, arg1);
    }

    @Override
    public void visitLineNumber(int lineNo, Label start) {
        this.lineNumber = lineNo;
    }

    @Override
    public void visitEnd() {
        if(!isJavaConfigMethod && !isConstructor) {
            NonJavaConfigMethod method = new NonJavaConfigMethod(methodName, modifiers);
            method.setLineNumber(this.lineNumber);
            lastAddedMethod = method;
            configClass.add(method);
        }

        if(lastAddedMethod != null)
            lastAddedMethod.setLineNumber(lineNumber);

        for(String methodName : scopedProxiesByMethodName.keySet())
            if(configClass.hasMethod(methodName))
                configClass.getMethod(methodName).addAnnotation(scopedProxiesByMethodName.get(methodName));
    }

}
