/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2003,2004 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.visitclass;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.bcel.classfile.AnnotationDefault;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Annotations;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.EnclosingMethod;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.InnerClass;
import org.apache.bcel.classfile.InnerClasses;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LineNumber;
import org.apache.bcel.classfile.LineNumberTable;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.ParameterAnnotations;
import org.apache.bcel.classfile.StackMapTable;
import org.apache.bcel.classfile.StackMapTableEntry;

import edu.umd.cs.findbugs.FindBugs;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.XClass;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;
import edu.umd.cs.findbugs.classfile.Global;
import edu.umd.cs.findbugs.classfile.IAnalysisCache;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.classfile.analysis.ClassInfo;
import edu.umd.cs.findbugs.classfile.analysis.FieldInfo;
import edu.umd.cs.findbugs.classfile.analysis.MethodInfo;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;

/**
 * Interface to make the use of a visitor pattern programming style possible.
 * I.e. a class that implements this interface can traverse the contents of a
 * Java class just by calling the `accept' method which all classes have.
 * <p/>
 * Implemented by wish of <A HREF="http://www.inf.fu-berlin.de/~bokowski">Boris
 * Bokowski</A>.
 * <p/>
 * If don't like it, blame him. If you do like it thank me 8-)
 *
 * @author <A HREF="http://www.inf.fu-berlin.de/~dahm">M. Dahm</A>
 * @version 970819
 */
public class PreorderVisitor extends BetterVisitor implements Constants2 {

    // Available when visiting a class
    private ConstantPool constantPool;

    private JavaClass thisClass;

    private ClassInfo thisClassInfo;

    private MethodInfo thisMethodInfo;

    private FieldInfo thisFieldInfo;

    private String className = "none";

    private String dottedClassName = "none";

    private String packageName = "none";

    private String sourceFile = "none";

    private String superclassName = "none";

    private String dottedSuperclassName = "none";

    // Available when visiting a method
    private boolean visitingMethod = false;

    private String methodSig = "none";

    private String dottedMethodSig = "none";

    private Method method = null;

    private String methodName = "none";

    private String fullyQualifiedMethodName = "none";

    // Available when visiting a field
    private Field field;

    private boolean visitingField = false;

    private String fullyQualifiedFieldName = "none";

    private String fieldName = "none";

    private String fieldSig = "none";

    private String dottedFieldSig = "none";

    private boolean fieldIsStatic;

    // Available when visiting a Code
    private Code code;

    protected String getStringFromIndex(int i) {
        ConstantUtf8 name = (ConstantUtf8) constantPool.getConstant(i);
        return name.getBytes();
    }

    protected int asUnsignedByte(byte b) {
        return 0xff & b;
    }

    /**
     * Return the current Code attribute; assuming one is being visited
     *
     * @return current code attribute
     */
    public Code getCode() {
        if (code == null)
            throw new IllegalStateException("Not visiting Code");
        return code;
    }

    public Set<String> getSurroundingCaughtExceptions(int pc) {
        return getSurroundingCaughtExceptions(pc, Integer.MAX_VALUE);
    }

    public Set<String> getSurroundingCaughtExceptions(int pc, int maxTryBlockSize) {
        HashSet<String> result = new HashSet<String>();
        if (code == null)
            throw new IllegalStateException("Not visiting Code");
        int size = maxTryBlockSize;
        if (code.getExceptionTable() == null)
            return result;
        for (CodeException catchBlock : code.getExceptionTable()) {
            int startPC = catchBlock.getStartPC();
            int endPC = catchBlock.getEndPC();
            if (pc >= startPC && pc <= endPC) {
                int thisSize = endPC - startPC;
                if (size > thisSize) {
                    result.clear();
                    size = thisSize;
                    Constant kind = constantPool.getConstant(catchBlock.getCatchType());
                    result.add("C" + catchBlock.getCatchType());
                } else if (size == thisSize)
                    result.add("C" + catchBlock.getCatchType());
            }
        }
        return result;
    }

    /**
     * Get lines of code in try block that surround pc
     *
     * @param pc
     * @return number of lines of code in try block
     */
    public int getSizeOfSurroundingTryBlock(int pc) {
        return getSizeOfSurroundingTryBlock(null, pc);
    }

    /**
     * Get lines of code in try block that surround pc
     *
     * @param pc
     * @return number of lines of code in try block
     */
    public int getSizeOfSurroundingTryBlock(String vmNameOfExceptionClass, int pc) {
        if (code == null)
            throw new IllegalStateException("Not visiting Code");
        return Util.getSizeOfSurroundingTryBlock(constantPool, code, vmNameOfExceptionClass, pc);
    }

    public CodeException getSurroundingTryBlock(int pc) {
        return getSurroundingTryBlock(null, pc);
    }

    public CodeException getSurroundingTryBlock(String vmNameOfExceptionClass, int pc) {
        if (code == null)
            throw new IllegalStateException("Not visiting Code");
        return Util.getSurroundingTryBlock(constantPool, code, vmNameOfExceptionClass, pc);
    }

    // Attributes
    @Override
    public void visitCode(Code obj) {
        code = obj;
        super.visitCode(obj);
        CodeException[] exceptions = obj.getExceptionTable();
        for (CodeException exception : exceptions)
            exception.accept(this);
        Attribute[] attributes = obj.getAttributes();
        for (Attribute attribute : attributes)
            attribute.accept(this);
        visitAfter(obj);
        code = null;
    }

    /**
     * Called after visiting a code attribute
     *
     * @param obj
     *            Code that was just visited
     */
    public void visitAfter(Code obj) {
    }

    // Constants
    @Override
    public void visitConstantPool(ConstantPool obj) {
        super.visitConstantPool(obj);
        Constant[] constant_pool = obj.getConstantPool();
        for (int i = 1; i < constant_pool.length; i++) {
            constant_pool[i].accept(this);
            byte tag = constant_pool[i].getTag();
            if ((tag == CONSTANT_Double) || (tag == CONSTANT_Long))
                i++;
        }
    }

    private void doVisitField(Field field) {
        if (visitingField)
            throw new IllegalStateException("visitField called when already visiting a field");
        visitingField = true;
        this.field = field;
        try {
            fieldName = fieldSig = dottedFieldSig = fullyQualifiedFieldName = null;
            thisFieldInfo = (FieldInfo) thisClassInfo.findField(getFieldName(), getFieldSig(), field.isStatic());
            assert thisFieldInfo !=  null : "Can't get field info for " + getFullyQualifiedFieldName();
            fieldIsStatic = field.isStatic();
            field.accept(this);
            Attribute[] attributes = field.getAttributes();
            for (Attribute attribute : attributes)
                attribute.accept(this);
        } finally {
            visitingField = false;
            this.field = null;
            this.thisFieldInfo = null;
        }
    }

    public void doVisitMethod(Method method) {
        if (visitingMethod)
            throw new IllegalStateException("doVisitMethod called when already visiting a method");
        visitingMethod = true;
        try {
            this.method = method;
            methodName = methodSig = dottedMethodSig = fullyQualifiedMethodName = null;
            thisMethodInfo = (MethodInfo) thisClassInfo.findMethod(getMethodName(), getMethodSig(), method.isStatic());
            assert thisMethodInfo != null : "Can't get method info for " + getFullyQualifiedMethodName();
            this.method.accept(this);
            Attribute[] attributes = method.getAttributes();
            for (Attribute attribute : attributes)
                attribute.accept(this);
        } finally {
            visitingMethod = false;
            this.method = null;
            this.thisMethodInfo = null;
        }
    }

    public boolean amVisitingMainMethod() {
        if (!visitingMethod)
            throw new IllegalStateException("Not visiting a method");
        if (!method.isStatic())
            return false;
        if (!getMethodName().equals("main"))
            return false;
        if (!getMethodSig().equals("([Ljava/lang/String;)V"))
            return false;
        return true;

    }

    // Extra classes (i.e. leaves in this context)
    @Override
    public void visitInnerClasses(InnerClasses obj) {
        super.visitInnerClasses(obj);
        InnerClass[] inner_classes = obj.getInnerClasses();
        for (InnerClass inner_class : inner_classes)
            inner_class.accept(this);
    }

    public void visitAfter(JavaClass obj) {
    }

    public boolean shouldVisit(JavaClass obj) {
        return true;
    }

    boolean visitMethodsInCallOrder;


   protected boolean isVisitMethodsInCallOrder() {
        return visitMethodsInCallOrder;
    }

    protected void setVisitMethodsInCallOrder(boolean visitMethodsInCallOrder) {
        this.visitMethodsInCallOrder = visitMethodsInCallOrder;
    }

    protected Iterable<Method> getMethodVisitOrder(JavaClass obj) {
        return Arrays.asList(obj.getMethods());
    }
    // General classes
    @Override
    public void visitJavaClass(JavaClass obj) {
        setupVisitorForClass(obj);
        if (shouldVisit(obj)) {
            constantPool.accept(this);
            Field[] fields = obj.getFields();
            Attribute[] attributes = obj.getAttributes();
            for (Field field : fields)
                doVisitField(field);
            boolean didInCallOrder = false;

            if (visitMethodsInCallOrder) {
                try {
                    IAnalysisCache analysisCache = Global.getAnalysisCache();

                    ClassDescriptor c = DescriptorFactory.createClassDescriptor(obj);

                    ClassContext classContext = analysisCache.getClassAnalysis(ClassContext.class, c);
                    didInCallOrder = true;
                    for (Method m : classContext.getMethodsInCallOrder())
                        doVisitMethod(m);

                } catch (CheckedAnalysisException e) {
                   AnalysisContext.logError("Error trying to visit methods in order", e);
                }
            }
            if (!didInCallOrder)
                for (Method m : getMethodVisitOrder(obj))
                    doVisitMethod(m);
            for (Attribute attribute : attributes)
                attribute.accept(this);
            visitAfter(obj);
        }
    }

    public void setupVisitorForClass(JavaClass obj) {
        constantPool = obj.getConstantPool();
        thisClass = obj;
        ConstantClass c = (ConstantClass) constantPool.getConstant(obj.getClassNameIndex());
        className = getStringFromIndex(c.getNameIndex());
        dottedClassName = className.replace('/', '.');
        packageName = obj.getPackageName();
        sourceFile = obj.getSourceFileName();
        dottedSuperclassName = obj.getSuperclassName();
        superclassName = dottedSuperclassName.replace('.', '/');

        ClassDescriptor cDesc = DescriptorFactory.createClassDescriptor(className);
        if (!FindBugs.isNoAnalysis())
        try {
            thisClassInfo = (ClassInfo) Global.getAnalysisCache().getClassAnalysis(XClass.class, cDesc);
        } catch (CheckedAnalysisException e) {
            throw new AssertionError("Can't find ClassInfo for " + cDesc);
        }

        super.visitJavaClass(obj);
    }

    @Override
    public void visitLineNumberTable(LineNumberTable obj) {
        super.visitLineNumberTable(obj);
        LineNumber[] line_number_table = obj.getLineNumberTable();
        for (LineNumber aLine_number_table : line_number_table)
            aLine_number_table.accept(this);
    }

    @Override
    public void visitLocalVariableTable(LocalVariableTable obj) {
        super.visitLocalVariableTable(obj);
        LocalVariable[] local_variable_table = obj.getLocalVariableTable();
        for (LocalVariable aLocal_variable_table : local_variable_table)
            aLocal_variable_table.accept(this);
    }

    // Accessors

    public XClass getXClass() {
        if (thisClassInfo == null)
            throw new AssertionError("XClass information not set");
        return thisClassInfo;
    }

    public ClassDescriptor getClassDescriptor() {
        return thisClassInfo;
    }

    public XMethod getXMethod() {
        return thisMethodInfo;
    }

    public MethodDescriptor getMethodDescriptor() {
        return thisMethodInfo;
    }

    public XField getXField() {
        return thisFieldInfo;
    }

    public FieldDescriptor getFieldDescriptor() {
        return thisFieldInfo;
    }

    /** Get the constant pool for the current or most recently visited class */
    public ConstantPool getConstantPool() {
        return constantPool;
    }

    /**
     * Get the slash-formatted class name for the current or most recently
     * visited class
     */
    public @SlashedClassName
    String getClassName() {
        return className;
    }

    /** Get the dotted class name for the current or most recently visited class */
    public @DottedClassName
    String getDottedClassName() {
        return dottedClassName;
    }

    /**
     * Get the (slash-formatted?) package name for the current or most recently
     * visited class
     */
    public String getPackageName() {
        return packageName;
    }

    /** Get the source file name for the current or most recently visited class */
    public String getSourceFile() {
        return sourceFile;
    }

    /**
     * Get the slash-formatted superclass name for the current or most recently
     * visited class
     */
    public @SlashedClassName
    String getSuperclassName() {
        return superclassName;
    }

    /**
     * Get the dotted superclass name for the current or most recently visited
     * class
     */
    public @DottedClassName
    String getDottedSuperclassName() {
        return dottedSuperclassName;
    }

    /** Get the JavaClass object for the current or most recently visited class */
    public JavaClass getThisClass() {
        return thisClass;
    }

    /** If currently visiting a method, get the method's fully qualified name */
    public String getFullyQualifiedMethodName() {
        if (!visitingMethod)
            throw new IllegalStateException("getFullyQualifiedMethodName called while not visiting method");
        if (fullyQualifiedMethodName == null) {
            getDottedSuperclassName();
            getMethodName();
            getDottedMethodSig();
            StringBuilder ref = new StringBuilder(5 + dottedClassName.length() + methodName.length() + dottedMethodSig.length());

            ref.append(dottedClassName).append(".").append(methodName).append(" : ").append(dottedMethodSig);
            fullyQualifiedMethodName = ref.toString();
        }
        return fullyQualifiedMethodName;
    }

    /**
     * is the visitor currently visiting a method?
     */
    public boolean visitingMethod() {
        return visitingMethod;
    }

    /**
     * is the visitor currently visiting a field?
     */
    public boolean visitingField() {
        return visitingField;
    }

    /** If currently visiting a field, get the field's Field object */
    public Field getField() {
        if (!visitingField)
            throw new IllegalStateException("getField called while not visiting field");
        return field;
    }

    /** If currently visiting a method, get the method's Method object */
    public Method getMethod() {
        if (!visitingMethod)
            throw new IllegalStateException("getMethod called while not visiting method");
        return method;
    }

    /** If currently visiting a method, get the method's name */
    public String getMethodName() {
        if (!visitingMethod)
            throw new IllegalStateException("getMethodName called while not visiting method");
        if (methodName == null)
            methodName = getStringFromIndex(method.getNameIndex());

        return methodName;
    }

    static Pattern argumentSignature = Pattern.compile("\\[*([BCDFIJSZ]|L[^;]*;)");

    public static int getNumberArguments(String signature) {
        int count = 0;
        int pos = 1;
        boolean inArray = false;

        while (true) {
            switch (signature.charAt(pos++)) {
            case ')':
                return count;
            case '[':
                if (!inArray)
                    count++;
                inArray = true;
                break;
            case 'L':
                if (!inArray)
                    count++;
                while (signature.charAt(pos) != ';')
                    pos++;
                pos++;
                inArray = false;
                break;
            default:
                if (!inArray)
                    count++;
                inArray = false;
                break;
            }
        }

    }

    public int getNumberMethodArguments() {
        return getNumberArguments(getMethodSig());
    }

    /**
     * If currently visiting a method, get the method's slash-formatted
     * signature
     */
    public String getMethodSig() {
        if (!visitingMethod)
            throw new IllegalStateException("getMethodSig called while not visiting method");
        if (methodSig == null)
            methodSig = getStringFromIndex(method.getSignatureIndex());
        return methodSig;
    }

    /** If currently visiting a method, get the method's dotted method signature */
    public String getDottedMethodSig() {
        if (!visitingMethod)
            throw new IllegalStateException("getDottedMethodSig called while not visiting method");
        if (dottedMethodSig == null)
            dottedMethodSig = getMethodSig().replace('/', '.');
        return dottedMethodSig;
    }

    /** If currently visiting a field, get the field's name */
    public String getFieldName() {
        if (!visitingField)
            throw new IllegalStateException("getFieldName called while not visiting field");
        if (fieldName == null)
            fieldName = getStringFromIndex(field.getNameIndex());

        return fieldName;
    }

    /** If currently visiting a field, get the field's slash-formatted signature */
    public String getFieldSig() {
        if (!visitingField)
            throw new IllegalStateException("getFieldSig called while not visiting field");
        if (fieldSig == null)
            fieldSig = getStringFromIndex(field.getSignatureIndex());
        return fieldSig;
    }

    /** If currently visiting a field, return whether or not the field is static */
    public boolean getFieldIsStatic() {
        if (!visitingField)
            throw new IllegalStateException("getFieldIsStatic called while not visiting field");
        return fieldIsStatic;
    }

    /** If currently visiting a field, get the field's fully qualified name */
    public String getFullyQualifiedFieldName() {
        if (!visitingField)
            throw new IllegalStateException("getFullyQualifiedFieldName called while not visiting field");
        if (fullyQualifiedFieldName == null)
            fullyQualifiedFieldName = getDottedClassName() + "." + getFieldName() + " : " + getFieldSig();
        return fullyQualifiedFieldName;
    }

    /** If currently visiting a field, get the field's dot-formatted signature */
    @Deprecated
    public String getDottedFieldSig() {
        if (!visitingField)
            throw new IllegalStateException("getDottedFieldSig called while not visiting field");
        if (dottedFieldSig == null)
            dottedFieldSig = fieldSig.replace('/', '.');
        return dottedFieldSig;
    }

    @Override
    public String toString() {
        if (visitingMethod)
            return this.getClass().getSimpleName() + " analyzing " + getClassName() + "." + getMethodName() + getMethodSig();
        else if (visitingField)
            return this.getClass().getSimpleName() + " analyzing " + getClassName() + "." + getFieldName();
        return this.getClass().getSimpleName() + " analyzing " + getClassName();
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitAnnotation(org.apache.bcel.classfile
     * .Annotations)
     */
    public void visitAnnotation(Annotations arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitAnnotationDefault(org.apache.bcel
     * .classfile.AnnotationDefault)
     */
    public void visitAnnotationDefault(AnnotationDefault arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitAnnotationEntry(org.apache.bcel
     * .classfile.AnnotationEntry)
     */
    public void visitAnnotationEntry(AnnotationEntry arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitEnclosingMethod(org.apache.bcel
     * .classfile.EnclosingMethod)
     */
    public void visitEnclosingMethod(EnclosingMethod arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitParameterAnnotation(org.apache
     * .bcel.classfile.ParameterAnnotations)
     */
    public void visitParameterAnnotation(ParameterAnnotations arg0) {
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitStackMapTable(org.apache.bcel.
     * classfile.StackMapTable)
     */
    public void visitStackMapTable(StackMapTable arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.apache.bcel.classfile.Visitor#visitStackMapTableEntry(org.apache.
     * bcel.classfile.StackMapTableEntry)
     */
    public void visitStackMapTableEntry(StackMapTableEntry arg0) {
        // TODO Auto-generated method stub

    }
}
