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

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.XClass;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.Global;

public class MutableStaticFields extends BytecodeScanningDetector {

    static String extractPackage(String c) {
        int i = c.lastIndexOf('/');
        if (i < 0) {
            return "";
        }
        return c.substring(0, i);
    }

    static boolean mutableSignature(String sig) {
        return sig.equals("Ljava/util/Hashtable;") || sig.equals("Ljava/util/Date;") || 
        sig.equals("Ljava/sql/Date;") || 
        sig.equals("Ljava/sql/Timestamp;") || 
        sig.charAt(0) == '[';
    }

    LinkedList<XField> seen = new LinkedList<XField>();

    boolean publicClass;

    boolean zeroOnTOS;

    boolean emptyArrayOnTOS;

    boolean inStaticInitializer;

    String packageName;

    Set<XField> readAnywhere = new HashSet<XField>();

    Set<XField> unsafeValue = new HashSet<XField>();

    Set<XField> notFinal = new HashSet<XField>();

    Set<XField> outsidePackage = new HashSet<XField>();
    Set<XField> needsRefactoringToBeFinal = new HashSet<XField>();
    
    Set<XField> writtenInMethod = new HashSet<XField>();
    Set<XField> writtenTwiceInMethod = new HashSet<XField>();

    Map<XField, SourceLineAnnotation> firstFieldUse = new HashMap<XField, SourceLineAnnotation>();

    private final BugReporter bugReporter;

    /**
     * Eclipse uses reflection to initialize NLS message bundles. Classes which
     * using this mechanism are usualy extending org.eclipse.osgi.util.NLS class
     * and contains lots of public static String fields which are used as
     * message constants. Unfortunately these fields cannot be final, so FB
     * reports tons of warnings for such Eclipse classes.
     */
    private boolean isEclipseNLS;

    public MutableStaticFields(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
    }

    @Override
    public void visit(JavaClass obj) {
        super.visit(obj);
        int flags = obj.getAccessFlags();
        publicClass = (flags & ACC_PUBLIC) != 0 && !getDottedClassName().startsWith("sun.");

        packageName = extractPackage(getClassName());
        isEclipseNLS = "org.eclipse.osgi.util.NLS".equals(obj.getSuperclassName());
    }

    @Override
    public void visit(Method obj) {
        zeroOnTOS = false;
        // System.out.println(methodName);
        inStaticInitializer = getMethodName().equals("<clinit>");
    }
    
    @Override
    public void visit(Code obj) {
        writtenInMethod.clear();
        writtenTwiceInMethod.clear();
        super.visit(obj);
        if (inStaticInitializer)
            needsRefactoringToBeFinal.addAll(writtenTwiceInMethod);
        writtenInMethod.clear();
        writtenTwiceInMethod.clear();
    }

    @Override
    public void sawOpcode(int seen) {
        // System.out.println("saw\t" + OPCODE_NAMES[seen] + "\t" + zeroOnTOS);
        switch (seen) {
        case GETSTATIC:
        case PUTSTATIC:

            XField xField = getXFieldOperand();
            if (xField == null) {
                break;
            }
            if (!interesting(xField)) {
                break;
            }

            boolean samePackage = packageName.equals(extractPackage(getClassConstantOperand()));
            boolean initOnly = seen == GETSTATIC || getClassName().equals(getClassConstantOperand()) && inStaticInitializer;
            boolean safeValue = seen == GETSTATIC || emptyArrayOnTOS
                    || AnalysisContext.currentXFactory().isEmptyArrayField(xField) || !mutableSignature(getSigConstantOperand());

            if (seen == GETSTATIC) {
                readAnywhere.add(xField);
            }
            if (seen == PUTSTATIC) {
                if (!writtenInMethod.add(xField))
                    writtenTwiceInMethod.add(xField);
            }

            if (!samePackage) {
                outsidePackage.add(xField);
            }

            if (!initOnly) {
                notFinal.add(xField);
            }

            if (!safeValue) {
                unsafeValue.add(xField);
            }

            // Remove inStaticInitializer check to report all source lines of
            // first use
            // doing so, however adds quite a bit of memory bloat.
            if (inStaticInitializer && !firstFieldUse.containsKey(xField)) {
                SourceLineAnnotation sla = SourceLineAnnotation.fromVisitedInstruction(this);
                firstFieldUse.put(xField, sla);
            }
            break;
        case ANEWARRAY:
        case NEWARRAY:
            if (zeroOnTOS) {
                emptyArrayOnTOS = true;
            }
            zeroOnTOS = false;
            return;
        case ICONST_0:
            zeroOnTOS = true;
            emptyArrayOnTOS = false;
            return;
        }
        zeroOnTOS = false;
        emptyArrayOnTOS = false;
    }

    private boolean interesting(XField f) {
        if (!f.isPublic() && !f.isProtected()) {
            return false;
        }
        if (!f.isStatic() || f.isSynthetic() || f.isVolatile()) {
            return false;
        }
        boolean isHashtable = f.getSignature().equals("Ljava/util/Hashtable;");
        boolean isArray = f.getSignature().charAt(0) == '[';
        if (f.isFinal() && !(isArray || isHashtable)) {
            return false;
        }
        return true;

    }

    @Override
    public void visit(Field obj) {
        super.visit(obj);
        int flags = obj.getAccessFlags();
        boolean isStatic = (flags & ACC_STATIC) != 0;
        if (!isStatic) {
            return;
        }
        boolean isVolatile = (flags & ACC_VOLATILE) != 0;
        if (isVolatile) {
            return;
        }
        boolean isFinal = (flags & ACC_FINAL) != 0;
        boolean isPublic = publicClass && (flags & ACC_PUBLIC) != 0;
        boolean isProtected = publicClass && (flags & ACC_PROTECTED) != 0;
        if (!isPublic && !isProtected) {
            return;
        }

        boolean isHashtable = getFieldSig().equals("Ljava/util/Hashtable;");
        boolean isArray = getFieldSig().charAt(0) == '[';

        if (isFinal && !(isHashtable || isArray)) {
            return;
        }
        if (isEclipseNLS && getFieldSig().equals("Ljava/lang/String;")) {
            return;
        }

        seen.add(getXField());
    }

    @Override
    public void report() {
        /*
         * for(Iterator i = unsafeValue.iterator(); i.hasNext(); ) {
         * System.out.println("Unsafe: " + i.next()); }
         */
        for (XField f : seen) {
            boolean isFinal = f.isFinal();
            String className = f.getClassName();
            String fieldSig = f.getSignature();
            String fieldName = f.getName();
            boolean couldBeFinal = !isFinal && !notFinal.contains(f);
            boolean isPublic = f.isPublic();
            boolean couldBePackage = !outsidePackage.contains(f);
            boolean movedOutofInterface = false;

            try {
                XClass xClass = Global.getAnalysisCache().getClassAnalysis(XClass.class, f.getClassDescriptor());
                movedOutofInterface = couldBePackage && xClass.isInterface();
            } catch (CheckedAnalysisException e) {
                assert true;
            }
            boolean isHashtable = fieldSig.equals("Ljava/util/Hashtable;");
            boolean isArray = fieldSig.charAt(0) == '[' && unsafeValue.contains(f);
            boolean isReadAnywhere = readAnywhere.contains(f);
            if (false) {
                System.out.println(className + "." + fieldName + " : " + fieldSig + "\t" + isHashtable + "\t" + isArray);
            }

            String bugType;
            int priority = NORMAL_PRIORITY;
            if (isFinal && !isHashtable && !isArray) {
                continue;
            } else if (movedOutofInterface) {
                bugType = "MS_OOI_PKGPROTECT";
            } else if (couldBePackage && couldBeFinal && (isHashtable || isArray)) {
                bugType = "MS_FINAL_PKGPROTECT";
            } else if (couldBeFinal && !isHashtable && !isArray) {
                bugType = "MS_SHOULD_BE_FINAL";
                if (needsRefactoringToBeFinal.contains(f))
                    bugType = "MS_SHOULD_BE_REFACTORED_TO_BE_FINAL";
                if (fieldName.equals(fieldName.toUpperCase()) || fieldSig.charAt(0) == 'L') {
                    priority = HIGH_PRIORITY;
                }
            } else if (couldBePackage) {
                bugType = "MS_PKGPROTECT";
            } else if (isHashtable) {
                bugType = "MS_MUTABLE_HASHTABLE";
                if (!isFinal) {
                    priority = HIGH_PRIORITY;
                }
            } else if (isArray) {
                bugType = "MS_MUTABLE_ARRAY";
                if (fieldSig.indexOf("L") >= 0 || !isFinal) {
                    priority = HIGH_PRIORITY;
                }
            } else if (!isFinal) {
                bugType = "MS_CANNOT_BE_FINAL";
            } else {
                throw new IllegalStateException("impossible");
            }
            if (!isReadAnywhere) {
                priority = LOW_PRIORITY;
            }

            BugInstance bug = new BugInstance(this, bugType, priority).addClass(className).addField(f);
            SourceLineAnnotation firstPC = firstFieldUse.get(f);
            if (firstPC != null) {
                bug.addSourceLine(firstPC);
            }
            bugReporter.reportBug(bug);

        }
    }
}
