/**************************************************************************************
 * Copyright (c) Jonas Bonér, Alexandre Vasseur. All rights reserved.                 *
 * http://aspectwerkz.codehaus.org                                                    *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the LGPL license      *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.transform.inlining;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.CodeVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.Constants;
import org.objectweb.asm.CodeAdapter;
import org.objectweb.asm.Type;
import org.codehaus.aspectwerkz.definition.SystemDefinition;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.expression.PointcutType;
import org.codehaus.aspectwerkz.joinpoint.management.JoinPointType;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.reflect.FieldInfo;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.transform.Context;
import org.codehaus.aspectwerkz.transform.TransformationUtil;
import org.codehaus.aspectwerkz.transform.TransformationConstants;

import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.List;

/**
 * Instruments method SET and GET join points by replacing PUTFIELD and GETFIELD instructions with invocations
 * of the compiled join point.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér </a>
 */
public class FieldSetGetClassAdapter extends ClassAdapter implements Constants, TransformationConstants {

    private final ContextImpl m_ctx;
    private final ClassLoader m_loader;
    private final ClassInfo m_callerClassInfo;

    /**
     * Creates a new instance.
     *
     * @param cv
     * @param loader
     * @param classInfo
     * @param ctx
     */
    public FieldSetGetClassAdapter(final ClassVisitor cv,
                                   final ClassLoader loader,
                                   final ClassInfo classInfo,
                                   final Context ctx) {
        super(cv);
        m_loader = loader;
        m_callerClassInfo = classInfo;
        m_ctx = (ContextImpl) ctx;
    }

    /**
     * Visits the caller methods.
     *
     * @param access
     * @param name
     * @param desc
     * @param exceptions
     * @param attrs
     * @return
     */
    public CodeVisitor visitMethod(final int access,
                                   final String name,
                                   final String desc,
                                   final String[] exceptions,
                                   final Attribute attrs) {

        if (INIT_METHOD_NAME.equals(name) ||
            CLINIT_METHOD_NAME.equals(name) ||
            name.startsWith(WRAPPER_METHOD_PREFIX)) {
            return super.visitMethod(access, name, desc, exceptions, attrs);
        }

        CodeVisitor mv = cv.visitMethod(access, name, desc, exceptions, attrs);
        return mv == null ? null : new ReplacePutFieldAndGetFieldInstructionCodeAdapter(
                mv,
                m_loader,
                m_callerClassInfo,
                m_ctx.getClassName(),
                name,
                desc
        );
    }

    /**
     * Replaces PUTFIELD and GETFIELD instructions with a call to the compiled JoinPoint instance.
     *
     * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér </a>
     */
    public class ReplacePutFieldAndGetFieldInstructionCodeAdapter extends CodeAdapter {

        private final ClassLoader m_loader;
        private final ClassInfo m_callerClassInfo;
        private final String m_callerClassName;
        private final String m_callerMethodName;
        private final String m_callerMethodDesc;
        private final MethodInfo m_callerMethodInfo;
        private int m_sequence = 1;

        /**
         * Creates a new instance.
         *
         * @param ca
         * @param loader
         * @param callerClassInfo
         * @param callerClassName
         * @param callerMethodName
         * @param callerMethodDesc
         */
        public ReplacePutFieldAndGetFieldInstructionCodeAdapter(final CodeVisitor ca,
                                                                final ClassLoader loader,
                                                                final ClassInfo callerClassInfo,
                                                                final String callerClassName,
                                                                final String callerMethodName,
                                                                final String callerMethodDesc) {
            super(ca);

            m_loader = loader;
            m_callerClassInfo = callerClassInfo;
            m_callerClassName = callerClassName;
            m_callerMethodName = callerMethodName;
            m_callerMethodDesc = callerMethodDesc;

            m_callerMethodInfo = m_callerClassInfo.getMethod(
                    AsmHelper.calculateMethodHash(
                            m_callerMethodName,
                            m_callerMethodDesc
                    )
            );

            if (m_callerMethodInfo == null) {
                throw new Error(
                        "caller method info metadata structure could not be build for method: "
                        + callerClassName
                        + '.'
                        + callerMethodName
                        + ':'
                        + callerMethodDesc
                );
            }
        }

        /**
         * Visits PUTFIELD and GETFIELD instructions.
         *
         * @param opcode
         * @param className
         * @param fieldName
         * @param fieldDesc
         */
        public void visitFieldInsn(final int opcode,
                                   final String className,
                                   final String fieldName,
                                   final String fieldDesc) {

            if (className.endsWith(JoinPointCompiler.JOIN_POINT_CLASS_SUFFIX) ||
                fieldName.startsWith(ASPECTWERKZ_PREFIX)) {
                super.visitFieldInsn(opcode, className, fieldName, fieldDesc);
                return;
            }

            Type fieldType = Type.getType(fieldDesc);

            int joinPointHash = AsmHelper.calculateFieldHash(fieldName, fieldDesc);
            ClassInfo classInfo = AsmClassInfo.getClassInfo(className.replace('/', '.'), m_loader);
            FieldInfo fieldInfo = getFieldInfo(classInfo, className, fieldName, fieldDesc, joinPointHash);

            if (opcode == PUTFIELD || opcode == PUTSTATIC) {
                ExpressionContext ctx = new ExpressionContext(PointcutType.SET, fieldInfo, m_callerMethodInfo);

                if (fieldFilter(m_ctx.getDefinitions(), ctx, fieldInfo)) {
                    super.visitFieldInsn(opcode, className, fieldName, fieldDesc);
                } else {
                    m_ctx.markAsAdvised();
                    m_sequence++; // single place of incrementation, is used in multiple places

                    createPutFieldWrapperMethod((opcode==PUTSTATIC), fieldName, fieldDesc);

                    String joinPointClassName = JoinPointCompiler.getJoinPointClassName(
                            m_callerClassName,
                            JoinPointType.FIELD_SET,
                            joinPointHash
                    );

                    // load the caller instance (this), or null if in a static context
                    // note that callee instance [optional] and args are already on the stack
                    if (Modifier.isStatic(m_callerMethodInfo.getModifiers())) {
                        visitInsn(ACONST_NULL);
                    } else {
                        visitVarInsn(ALOAD, 0);
                    }

                    // add the call to the join point
                    super.visitMethodInsn(
                            INVOKESTATIC,
                            joinPointClassName,
                            INVOKE_METHOD_NAME,
                            TransformationUtil.getInvokeSignatureForFieldJoinPoints(
                                    fieldInfo.getModifiers(), fieldDesc, m_callerClassName, className
                            )
                    );
                    super.visitInsn(POP);// field is set by the JP

                    // emit the joinpoint
                    m_ctx.addEmittedInlinedJoinPoint(
                            new ContextImpl.EmittedInlinedJoinPoint(
                                    JoinPointType.FIELD_SET,
                                    m_callerClassName,
                                    m_callerMethodName,
                                    m_callerMethodDesc,
                                    m_callerMethodInfo.getModifiers(),
                                    className,
                                    fieldName,
                                    fieldDesc,
                                    fieldInfo.getModifiers(),
                                    m_sequence,
                                    joinPointHash,
                                    joinPointClassName
                            )
                    );
                }
            } else if (opcode == GETFIELD || opcode == GETSTATIC) {
                ExpressionContext ctx = new ExpressionContext(PointcutType.GET, fieldInfo, m_callerMethodInfo);

                if (fieldFilter(m_ctx.getDefinitions(), ctx, fieldInfo)) {
                    super.visitFieldInsn(opcode, className, fieldName, fieldDesc);
                } else {
                    m_ctx.markAsAdvised();
                    m_sequence++; // single place of incrementation, is used in multiple places

                    createGetFieldWrapperMethod((opcode==GETSTATIC), fieldName, fieldDesc);

                    String joinPointClassName = JoinPointCompiler.getJoinPointClassName(
                            m_callerClassName,
                            JoinPointType.FIELD_GET,
                            joinPointHash
                    );

                    // if static context pop the 'this' instance and load NULL
                    if (Modifier.isStatic(m_callerMethodInfo.getModifiers())) {
                        visitInsn(ACONST_NULL);
                    }

                    // no param to field, so pass a default value to the invoke method
                    AsmHelper.addDefaultValue(this, fieldType);

                    // if static context load NULL else 'this'
                    if (Modifier.isStatic(m_callerMethodInfo.getModifiers())) {
                        visitInsn(ACONST_NULL);
                    } else {
                        visitVarInsn(ALOAD, 0);
                    }

                    // add the call to the join point
                    super.visitMethodInsn(
                            INVOKESTATIC,
                            joinPointClassName,
                            INVOKE_METHOD_NAME,
                            TransformationUtil.getInvokeSignatureForFieldJoinPoints(
                                    fieldInfo.getModifiers(), fieldDesc, m_callerClassName, className
                            )
                    );
                    //super.visitInsn(POP);//pop the field value returned from jp.invoke for now

                    // emit the joinpoint
                    m_ctx.addEmittedInlinedJoinPoint(
                            new ContextImpl.EmittedInlinedJoinPoint(
                                    JoinPointType.FIELD_GET,
                                    m_callerClassName,
                                    m_callerMethodName,
                                    m_callerMethodDesc,
                                    m_callerMethodInfo.getModifiers(),
                                    className,
                                    fieldName,
                                    fieldDesc,
                                    fieldInfo.getModifiers(),
                                    m_sequence,
                                    joinPointHash,
                                    joinPointClassName
                            )
                    );
                }
            } else {
                super.visitFieldInsn(opcode, className, fieldName, fieldDesc);
            }
        }

        /**
         * Creates a public wrapper method that delegates to the PUTFIELD instruction of the non-public field.
         * Static method if field is static (PUTSTATIC instr)
         *
         * @param isStaticField
         * @param name
         * @param desc
         */
        private void createPutFieldWrapperMethod(boolean isStaticField,
                                                 final String name,
                                                 final String desc) {
            Type fieldType = Type.getType(desc);

            StringBuffer signature = new StringBuffer();
            signature.append('(');
            signature.append(desc);
            signature.append(')');
            signature.append('V');

            int modifiers = ACC_SYNTHETIC;
            if (isStaticField) {
                modifiers |= ACC_STATIC;
            }

            CodeVisitor mv = FieldSetGetClassAdapter.this.visitMethod(
                    modifiers,
                    TransformationUtil.getWrapperMethodName(
                            name, m_sequence, m_callerClassName, PUTFIELD_WRAPPER_METHOD_PREFIX
                    ),
                    signature.toString(),
                    new String[]{},
                    null
            );

            if (isStaticField) {
                AsmHelper.loadArgumentTypes(mv, new Type[]{fieldType}, true);
                mv.visitFieldInsn(PUTSTATIC, m_callerClassName, name, desc);
            } else {
                mv.visitVarInsn(ALOAD, 0);
                AsmHelper.loadArgumentTypes(mv, new Type[]{fieldType}, false);
                mv.visitFieldInsn(PUTFIELD, m_callerClassName, name, desc);
            }

            AsmHelper.addReturnStatement(mv, Type.VOID_TYPE);
            mv.visitMaxs(0, 0);
        }


        /**
         * Creates a public wrapper method that delegates to the GETFIELD instruction of the non-public field.
         *
         * @param isStaticField
         * @param name
         * @param desc
         */
        private void createGetFieldWrapperMethod(final boolean isStaticField,
                                                 final String name,
                                                 final String desc) {
            Type fieldType = Type.getType(desc);

            StringBuffer signature = new StringBuffer();
            signature.append('(');
            signature.append(')');
            signature.append(desc);

            int modifiers = ACC_SYNTHETIC;
            if (isStaticField) {
                modifiers |= ACC_STATIC;
            }

            CodeVisitor mv = FieldSetGetClassAdapter.this.visitMethod(
                    modifiers,
                    TransformationUtil.getWrapperMethodName(
                            name, m_sequence, m_callerClassName, GETFIELD_WRAPPER_METHOD_PREFIX
                    ),
                    signature.toString(),
                    new String[]{},
                    null
            );

            if (isStaticField) {
                mv.visitFieldInsn(GETSTATIC, m_callerClassName, name, desc);
            } else {
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, m_callerClassName, name, desc);
            }

            AsmHelper.addReturnStatement(mv, fieldType);
            mv.visitMaxs(0, 0);
        }

        /**
         * Returns the field info.
         *
         * @param classInfo
         * @param className
         * @param fieldName
         * @param fieldDesc
         * @param joinPointHash
         * @return
         */
        private FieldInfo getFieldInfo(final ClassInfo classInfo,
                                       final String className,
                                       final String fieldName,
                                       final String fieldDesc,
                                       final int joinPointHash) {
            FieldInfo fieldInfo = classInfo.getField(joinPointHash);
            if (fieldInfo == null) {
                // lookup in the class hierarchy
                ClassInfo superClassInfo = classInfo.getSuperClass();
                while (superClassInfo != null) {
                    fieldInfo = superClassInfo.getField(joinPointHash);
                    if (fieldInfo == null) {
                        // go up in the hierarchy
                        superClassInfo = superClassInfo.getSuperClass();
                    } else {
                        break;
                    }
                }
                if (fieldInfo == null) {
                    throw new Error(
                            "field info metadata structure could not be build for field: "
                            + className
                            + '.'
                            + fieldName
                            + ':'
                            + fieldDesc
                    );
                }
            }
            return fieldInfo;
        }

        /**
         * Filters out the fields that are not eligible for transformation.
         *
         * @param definitions
         * @param ctx
         * @param fieldInfo
         * @return boolean true if the field should be filtered out
         */
        public boolean fieldFilter(final List definitions,
                                   final ExpressionContext ctx,
                                   final FieldInfo fieldInfo) {
            if (fieldInfo.getName().startsWith(ORIGINAL_METHOD_PREFIX)) {
                return true;
            }
            for (Iterator it = definitions.iterator(); it.hasNext();) {
                if (((SystemDefinition) it.next()).hasPointcut(ctx)) {
                    return false;
                } else {
                    continue;
                }
            }
            return true;
        }
    }
}