/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2003-2008 University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.classfile.engine;

import java.util.BitSet;
import java.util.HashSet;
import java.util.TreeSet;

import javax.annotation.CheckForNull;

import org.apache.bcel.Constants;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import edu.umd.cs.findbugs.ba.SignatureParser;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.ICodeBaseEntry;
import edu.umd.cs.findbugs.classfile.InvalidClassFileFormatException;
import edu.umd.cs.findbugs.classfile.analysis.AnnotationValue;
import edu.umd.cs.findbugs.classfile.analysis.ClassInfo;
import edu.umd.cs.findbugs.classfile.analysis.ClassInfo.Builder;
import edu.umd.cs.findbugs.classfile.analysis.ClassNameAndSuperclassInfo;
import edu.umd.cs.findbugs.classfile.analysis.FieldInfo;
import edu.umd.cs.findbugs.classfile.analysis.MethodInfo;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;
import edu.umd.cs.findbugs.util.ClassName;

/**
 * @author William Pugh
 */
public class ClassParserUsingASM implements ClassParserInterface {

    // static final boolean NO_SHIFT_INNER_CLASS_CTOR =
    // SystemProperties.getBoolean("classparser.noshift");

    private static final BitSet RETURN_OPCODE_SET = new BitSet();
    static {
        RETURN_OPCODE_SET.set(Constants.ARETURN);
        RETURN_OPCODE_SET.set(Constants.IRETURN);
        RETURN_OPCODE_SET.set(Constants.LRETURN);
        RETURN_OPCODE_SET.set(Constants.DRETURN);
        RETURN_OPCODE_SET.set(Constants.FRETURN);
        RETURN_OPCODE_SET.set(Constants.RETURN);
    }

    private final ClassReader classReader;

    private @SlashedClassName
    String slashedClassName;

    private final ClassDescriptor expectedClassDescriptor;

    private final ICodeBaseEntry codeBaseEntry;



    enum StubState {
        INITIAL, LOADED_STUB, INITIALIZE_RUNTIME
    }

    enum IdentityMethodState {
        INITIAL, LOADED_PARAMETER, NOT;
    }
    enum ParameterLoadState {
        OTHER, LOADED_THIS, LOADED_THIS_AND_PARAMETER;
    }
    public ClassParserUsingASM(ClassReader classReader, @CheckForNull ClassDescriptor expectedClassDescriptor,
            ICodeBaseEntry codeBaseEntry) {
        this.classReader = classReader;
        this.expectedClassDescriptor = expectedClassDescriptor;
        this.codeBaseEntry = codeBaseEntry;
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * edu.umd.cs.findbugs.classfile.engine.ClassParserInterface#parse(edu.umd
     * .cs.findbugs.classfile.analysis.ClassNameAndSuperclassInfo.Builder)
     */
    public void parse(final ClassNameAndSuperclassInfo.Builder cBuilder) throws InvalidClassFileFormatException {

        cBuilder.setCodeBaseEntry(codeBaseEntry);

        final TreeSet<ClassDescriptor> calledClassSet = new TreeSet<ClassDescriptor>();

        classReader.accept(new ClassVisitor() {

            boolean isInnerClass = false;

            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                ClassParserUsingASM.this.slashedClassName = name;
                cBuilder.setClassfileVersion(version >>> 16, version & 0xffff);
                cBuilder.setAccessFlags(access);
                cBuilder.setClassDescriptor(DescriptorFactory.createClassDescriptor(name));
                cBuilder.setInterfaceDescriptorList(DescriptorFactory.createClassDescriptor(interfaces));
                if (superName != null)
                    cBuilder.setSuperclassDescriptor(DescriptorFactory.createClassDescriptor(superName));
                if (cBuilder instanceof ClassInfo.Builder) {
                    ((ClassInfo.Builder) cBuilder).setSourceSignature(signature);
                }
            }

            public org.objectweb.asm.AnnotationVisitor visitAnnotation(String desc, boolean isVisible) {
                if (cBuilder instanceof ClassInfo.Builder) {
                    AnnotationValue value = new AnnotationValue(desc);
                    ((ClassInfo.Builder) cBuilder).addAnnotation(desc, value);
                    return value.getAnnotationVisitor();
                }
                return null;
            }

            public void visitAttribute(Attribute arg0) {
                // TODO Auto-generated method stub

            }

            public void visitEnd() {
                // TODO Auto-generated method stub

            }

            public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                if (name.equals("this$0"))
                    isInnerClass = true;

                if (desc == null)
                    throw new NullPointerException("Description cannot be null");
                if (cBuilder instanceof ClassInfo.Builder) {
                    final ClassInfo.Builder cBuilder2 = (ClassInfo.Builder) cBuilder;
                    if ((access & Opcodes.ACC_VOLATILE) != 0 || desc.contains("util/concurrent"))
                        cBuilder2.setUsesConcurrency();
                    final FieldInfo.Builder fBuilder = new FieldInfo.Builder(slashedClassName, name, desc, access);
                    fBuilder.setSourceSignature(signature);
                    return new AbstractFieldAnnotationVisitor() {

                        public org.objectweb.asm.AnnotationVisitor visitAnnotation(final String desc, boolean visible) {
                            AnnotationValue value = new AnnotationValue(desc);
                            fBuilder.addAnnotation(desc, value);
                            return value.getAnnotationVisitor();
                        }

                        public void visitEnd() {
                            cBuilder2.addFieldDescriptor(fBuilder.build());

                        }

                    };

                }
                return null;
            }

            public void visitInnerClass(String name, String outerName, String innerName, int access) {
                if (name.equals(slashedClassName) && outerName != null) {
                    if (cBuilder instanceof ClassInfo.Builder) {
                        ClassDescriptor outerClassDescriptor = DescriptorFactory.createClassDescriptor(outerName);
                        ((ClassInfo.Builder) cBuilder).setImmediateEnclosingClass(outerClassDescriptor);
                        ((ClassInfo.Builder) cBuilder).setAccessFlags(access);
                    }

                }

            }

            public MethodVisitor visitMethod(final int access, final String methodName, final String methodDesc,
                    String signature, String[] exceptions) {
                if (cBuilder instanceof ClassInfo.Builder) {
                    final MethodInfo.Builder mBuilder = new MethodInfo.Builder(slashedClassName, methodName, methodDesc, access);
                    mBuilder.setSourceSignature(signature);
                    mBuilder.setThrownExceptions(exceptions);
                    if ((access & Opcodes.ACC_SYNCHRONIZED) != 0)
                        mBuilder.setUsesConcurrency();

                    return new AbstractMethodVisitor() {

                        boolean sawReturn = (access & Opcodes.ACC_NATIVE) != 0;

                        boolean sawNormalThrow = false;

                        boolean sawUnsupportedThrow = false;

                        boolean sawSystemExit = false;

                        boolean sawBranch = false;

                        boolean sawBackBranch = false;

                        int methodCallCount = 0;
                        int fieldInstructionCount = 0;

                        boolean sawStubThrow = false;

                        boolean justSawInitializationOfUnsupportedOperationException;

                        boolean isBridge = (access & Opcodes.ACC_SYNTHETIC) != 0 && (access & Opcodes.ACC_BRIDGE) != 0;

                        String bridgedMethodSignature;

                        IdentityMethodState identityState =
                                IdentityMethodState.INITIAL;

                        ParameterLoadState parameterLoadState = ParameterLoadState.OTHER;

                        int parameterForLoadState;

                        StubState stubState = StubState.INITIAL;

                        boolean isAccessMethod = methodName.startsWith("access$");

                        String accessOwner, accessName, accessDesc;

                        boolean accessForField;
                        boolean accessIsStatic;

                        HashSet<Label> labelsSeen = new HashSet<Label>();

                        boolean isStatic() {
                            return (access & Opcodes.ACC_STATIC) != 0;
                        }

                        @Override
                        public
                        void visitLocalVariable(String name,
                                String desc,
                                String signature,
                                Label start,
                                Label end,
                                int index) {
                            mBuilder.setVariableHasName(index);

                        }

                        @Override
                        public void visitLdcInsn(Object cst) {
                            if (cst.equals("Stub!"))
                                stubState = StubState.LOADED_STUB;
                            else
                                stubState = StubState.INITIAL;
                            identityState = IdentityMethodState.NOT;
                        }

                        @Override
                        public void visitInsn(int opcode) {
                            switch (opcode) {
                            case Constants.MONITORENTER:
                                mBuilder.setUsesConcurrency();
                                break;
                            case Constants.ARETURN:
                            case Constants.IRETURN:
                            case Constants.LRETURN:
                            case Constants.DRETURN:
                            case Constants.FRETURN:
                                if (identityState == IdentityMethodState.LOADED_PARAMETER)
                                    mBuilder.setIsIdentity();
                                sawReturn = true;
                                break;
                            case Constants.RETURN:
                                sawReturn = true;
                                break;
                            case Constants.ATHROW:
                                if (stubState == StubState.INITIALIZE_RUNTIME)
                                    sawStubThrow = true;
                                else if (justSawInitializationOfUnsupportedOperationException)
                                    sawUnsupportedThrow = true;
                                else
                                    sawNormalThrow = true;
                                break;
                            }

                            resetState();
                        }

                        public void resetState() {
                            stubState = StubState.INITIAL;
                        }

                        @Override
                        public void visitSomeInsn() {
                            identityState = IdentityMethodState.NOT;
                            parameterLoadState = ParameterLoadState.OTHER;
                            resetState();
                        }

                        @Override
                        public void visitVarInsn(int opcode, int var) {

                            boolean match = false;
                            if (parameterLoadState == ParameterLoadState.OTHER && !isStatic() && var == 0) {
                                    parameterLoadState = ParameterLoadState.LOADED_THIS;

                                    match = true;
                            }
                            else if (parameterLoadState == ParameterLoadState.LOADED_THIS  && var > 0){
                                parameterLoadState = ParameterLoadState.LOADED_THIS_AND_PARAMETER;
                                parameterForLoadState = var;
                                match = true;
                            }

                            if (identityState == IdentityMethodState.INITIAL) {
                                match = true;
                                if (var > 0 || isStatic())
                                    identityState = IdentityMethodState.LOADED_PARAMETER;
                                else
                                    identityState = IdentityMethodState.NOT;

                            }
                            if (!match)
                                visitSomeInsn();
                        }

                        @Override
                        public void visitFieldInsn(int opcode,
                                            String owner,
                                            String name,
                                            String desc) {
                            if (opcode == Opcodes.PUTFIELD && parameterLoadState == ParameterLoadState.LOADED_THIS_AND_PARAMETER
                                    && owner.equals(slashedClassName) && name.startsWith("this$")) {
                                mBuilder.setVariableIsSynthetic(parameterForLoadState);
                            }
                            fieldInstructionCount++;

                            if (isAccessMethod && this.accessOwner == null) {
                                this.accessOwner = owner;
                                this.accessName = name;
                                this.accessDesc = desc;
                                this.accessIsStatic = opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC;
                                this.accessForField = true;
                            }
                            visitSomeInsn();
                        }

                        public org.objectweb.asm.AnnotationVisitor visitAnnotation(final String desc, boolean visible) {
                            AnnotationValue value = new AnnotationValue(desc);
                            mBuilder.addAnnotation(desc, value);
                            return value.getAnnotationVisitor();
                        }

                        @Override
                        public void visitMethodInsn(int opcode, String owner, String name, String desc) {
                            identityState = IdentityMethodState.NOT;
                            methodCallCount++;
                            if (isAccessMethod && this.accessOwner == null) {
                                this.accessOwner = owner;
                                this.accessName = name;
                                this.accessDesc = desc;
                                this.accessIsStatic = opcode == Opcodes.INVOKESTATIC;
                                this.accessForField = false;
                            }
                            if (stubState == StubState.LOADED_STUB && opcode == Opcodes.INVOKESPECIAL
                                    && owner.equals("java/lang/RuntimeException") && name.equals("<init>"))
                                stubState = StubState.INITIALIZE_RUNTIME;
                            else
                                stubState = StubState.INITIAL;
                            if (owner.startsWith("java/util/concurrent"))
                                mBuilder.setUsesConcurrency();
                            if (opcode == Opcodes.INVOKEINTERFACE)
                                return;

                            if (owner.charAt(0) == '[' && owner.charAt(owner.length() - 1) != ';') {
                                // primitive array
                                return;
                            }
                            if (opcode == Opcodes.INVOKESTATIC && owner.equals("java/lang/System") && name.equals("exit")
                                    && !sawReturn)
                                sawSystemExit = true;
                            justSawInitializationOfUnsupportedOperationException = opcode == Opcodes.INVOKESPECIAL
                                    && owner.equals("java/lang/UnsupportedOperationException") && name.equals("<init>");

                            if (isBridge && bridgedMethodSignature == null)
                                switch (opcode) {
                                case Opcodes.INVOKEVIRTUAL:
                                case Opcodes.INVOKESPECIAL:
                                case Opcodes.INVOKESTATIC:
                                case Opcodes.INVOKEINTERFACE:
                                    if (desc != null && name.equals(methodName))
                                        bridgedMethodSignature = desc;
                                }

                            // System.out.println("Call from " +
                            // ClassParserUsingASM.this.slashedClassName +
                            // " to " + owner + " : " + desc);
                            if (desc == null || desc.indexOf('[') == -1 && desc.indexOf('L') == -1)
                                return;
                            if (ClassParserUsingASM.this.slashedClassName.equals(owner))
                                return;
                            ClassDescriptor classDescriptor = DescriptorFactory.instance().getClassDescriptor(owner);
                            calledClassSet.add(classDescriptor);

                        }

                        private void sawBranchTo(Label label) {
                            sawBranch = true;
                            if (labelsSeen.contains(label))
                                sawBackBranch = true;
                        }

                        @Override
                        public void visitJumpInsn(int opcode, Label label) {
                            sawBranchTo(label);
                            identityState = IdentityMethodState.NOT;
                            super.visitJumpInsn(opcode, label);
                        }

                        @Override
                        public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
                            sawBranchTo(dflt);
                            for (Label lbl : labels)
                                sawBranchTo(lbl);
                            identityState = IdentityMethodState.NOT;
                            super.visitLookupSwitchInsn(dflt, keys, labels);
                        }

                        @Override
                        public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) {
                            sawBranchTo(dflt);
                            for (Label lbl : labels)
                                sawBranchTo(lbl);
                            identityState = IdentityMethodState.NOT;
                            super.visitTableSwitchInsn(min, max, dflt, labels);
                        }
                        
                        @Override
                        public void visitLabel(Label label) {
                            labelsSeen.add(label);
                            super.visitLabel(label);

                        }

                        public void visitEnd() {
                            labelsSeen.clear();
                            if (isAccessMethod && accessOwner != null) {
                                if (!accessForField && methodCallCount == 1) {
                                    mBuilder.setAccessMethodForMethod(accessOwner, accessName, accessDesc, accessIsStatic);
                                } else if(accessForField && fieldInstructionCount == 1) {
                                    boolean isSetter = methodDesc.endsWith(")V");
                                    int numArg = new SignatureParser(methodDesc).getNumParameters();
                                    int expected = 0;
                                    if (!accessIsStatic) expected++;
                                    if (isSetter) expected++;
                                    boolean OK;
                                    if (isSetter)
                                        OK = methodDesc.substring(1).startsWith(ClassName.toSignature(accessOwner) + accessDesc);
                                    else
                                        OK = methodDesc.substring(1).startsWith(ClassName.toSignature(accessOwner));
                                    if (numArg == expected && OK)
                                        mBuilder.setAccessMethodForField(accessOwner, accessName, accessDesc, accessIsStatic);
                                }
                            }
                            if (sawBackBranch)
                                mBuilder.setHasBackBranch();
                            boolean sawThrow = sawNormalThrow | sawUnsupportedThrow | sawStubThrow;
                            if (sawThrow && !sawReturn || sawSystemExit && !sawBranch) {

                                mBuilder.setIsUnconditionalThrower();
                                if (!sawReturn && !sawNormalThrow) {
                                    if (sawUnsupportedThrow)
                                        mBuilder.setUnsupported();
                                    if (sawStubThrow) {
                                        mBuilder.addAccessFlags(Constants.ACC_SYNTHETIC);
                                        mBuilder.setIsStub();

                                    }
                                }
                                // else
                                // System.out.println(slashedClassName+"."+methodName+methodDesc
                                // + " is thrower");
                            }
                            mBuilder.setNumberMethodCalls(methodCallCount);
                            MethodInfo methodInfo = mBuilder.build();
                            Builder classBuilder = (ClassInfo.Builder) cBuilder;
                            if (isBridge && bridgedMethodSignature != null && !bridgedMethodSignature.equals(methodDesc))
                                classBuilder.addBridgeMethodDescriptor(methodInfo, bridgedMethodSignature);
                            else
                                classBuilder.addMethodDescriptor(methodInfo);

                            if (methodInfo.usesConcurrency())
                                classBuilder.setUsesConcurrency();
                            if (methodInfo.isStub())
                                classBuilder.setHasStubs();
                        }

                        public org.objectweb.asm.AnnotationVisitor visitParameterAnnotation(int parameter, String desc,
                                boolean visible) {
                            AnnotationValue value = new AnnotationValue(desc);
                            mBuilder.addParameterAnnotation(parameter, desc, value);
                            return value.getAnnotationVisitor();
                        }
                    };

                }
                return null;
            }

            public void visitOuterClass(String owner, String name, String desc) {

            }

            public void visitSource(String arg0, String arg1) {
                if (cBuilder instanceof ClassInfo.Builder) {
                    ((ClassInfo.Builder) cBuilder).setSource(arg0);
                }

            }
        }, ClassReader.SKIP_FRAMES);
        HashSet<ClassDescriptor> referencedClassSet = new HashSet<ClassDescriptor>();

        // collect class references

        int constantPoolCount = classReader.readUnsignedShort(8);
        int offset = 10;
        char[] buf = new char[1024];
        // System.out.println("constant pool count: " + constantPoolCount);
        for (int count = 1; count < constantPoolCount; count++) {
            int tag = classReader.readByte(offset);

            int size;
            switch (tag) {
            case Constants.CONSTANT_Methodref:
            case Constants.CONSTANT_InterfaceMethodref:
            case Constants.CONSTANT_Fieldref:
            case Constants.CONSTANT_Integer:
            case Constants.CONSTANT_Float:
            case Constants.CONSTANT_NameAndType:
                size = 5;
                break;
            case Constants.CONSTANT_Long:
            case Constants.CONSTANT_Double:
                size = 9;
                count++;
                break;
            case Constants.CONSTANT_Utf8:
                size = 3 + classReader.readUnsignedShort(offset + 1);
                break;
            case Constants.CONSTANT_Class:
                @SlashedClassName
                String className = classReader.readUTF8(offset + 1, buf);
                if (className.indexOf('[') >= 0) {
                    ClassParser.extractReferencedClassesFromSignature(referencedClassSet, className);
                } else if (ClassName.isValidClassName(className)) {
                    ClassDescriptor classDescriptor = DescriptorFactory.instance().getClassDescriptor(className);
                    referencedClassSet.add(classDescriptor);
                }
                size = 3;
                break;
            // case ClassWriter.CLASS:
            // case ClassWriter.STR:
            case Constants.CONSTANT_String:
                size = 3;
                break;
            default:
                throw new IllegalStateException("Unexpected tag of " + tag + " at offset " + offset + " while parsing "
                        + slashedClassName + " from " + codeBaseEntry);
            }
            // System.out.println(count + "@" + offset + " : [" + tag
            // +"] size="+size);
            offset += size;
        }
        cBuilder.setCalledClassDescriptors(calledClassSet);
        cBuilder.setReferencedClassDescriptors(referencedClassSet);
    }

    public void parse(ClassInfo.Builder builder) throws InvalidClassFileFormatException {
        parse((ClassNameAndSuperclassInfo.Builder) builder);

    }
}
