/*
 * 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.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.bcel.classfile.Code;
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.FieldAnnotation;
import edu.umd.cs.findbugs.MethodAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;

public class InitializationChain extends BytecodeScanningDetector {
    Set<String> requires = new TreeSet<String>();

    Map<String, Set<String>> classRequires = new TreeMap<String, Set<String>>();

    
   
    private BugReporter bugReporter;

    private Map<XMethod, Set<XField>> staticFieldsRead = new HashMap<XMethod, Set<XField>>();
    private Set<XField> staticFieldsReadInAnyConstructor = new HashSet<XField>();
    private Set<XField> fieldsReadInThisConstructor = new HashSet<XField>();

    private Set<XMethod> constructorsInvokedInStaticInitializer = new HashSet<XMethod>();
    private List<InvocationInfo> invocationInfo = new ArrayList<InvocationInfo>();
    private Set<XField> warningGiven = new HashSet<XField>();

    private InvocationInfo lastInvocation;
   
    static class InvocationInfo {
        public InvocationInfo(XMethod constructor, int pc) {
            this.constructor = constructor;
            this.pc = pc;
        }
        XMethod constructor;
        int pc;
        XField field;
    }

    private static final boolean DEBUG = SystemProperties.getBoolean("ic.debug");

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

    @Override
    protected Iterable<Method> getMethodVisitOrder(JavaClass obj) {
        ArrayList<Method> visitOrder = new ArrayList<Method>();
        Method staticInitializer = null;
        for(Method m : obj.getMethods()) {
            String name = m.getName();
            if (name.equals("<clinit>"))
                staticInitializer = m;
            else if (name.equals("<init>"))
                visitOrder.add(m);
            
        }
        if (staticInitializer != null)
            visitOrder.add(staticInitializer);
        return visitOrder;
    }
    
    
    @Override
    public void visit(Code obj) {
        fieldsReadInThisConstructor  = new HashSet<XField>();
        super.visit(obj);
        staticFieldsRead.put(getXMethod(), fieldsReadInThisConstructor);
        requires.remove(getDottedClassName());
        if (getDottedClassName().equals("java.lang.System")) {
            requires.add("java.io.FileInputStream");
            requires.add("java.io.FileOutputStream");
            requires.add("java.io.BufferedInputStream");
            requires.add("java.io.BufferedOutputStream");
            requires.add("java.io.PrintStream");
        }
        if (!requires.isEmpty()) {
            classRequires.put(getDottedClassName(), requires);
            requires = new TreeSet<String>();
        }
    }

    @Override
    public void visitAfter(JavaClass obj) {
        
        staticFieldsRead.clear();
        
        staticFieldsReadInAnyConstructor.clear();
        fieldsReadInThisConstructor.clear();

        constructorsInvokedInStaticInitializer.clear();
        invocationInfo.clear();
        lastInvocation = null;

    }

    @Override
    public void sawOpcode(int seen) {
        InvocationInfo prev = lastInvocation;
        lastInvocation = null;
        if (getMethodName().equals("<init>")) {
            if (seen == GETSTATIC && getClassConstantOperand().equals(getClassName())) {
                staticFieldsReadInAnyConstructor.add(getXFieldOperand());
                fieldsReadInThisConstructor.add(getXFieldOperand());
            }
            return;
        }

        if (seen == INVOKESPECIAL && getNameConstantOperand().equals("<init>") &&  getClassConstantOperand().equals(getClassName())) {
            
            XMethod m = getXMethodOperand();
            Set<XField> read = staticFieldsRead.get(m);
            if (constructorsInvokedInStaticInitializer.add(m) && read != null && !read.isEmpty()) {
                lastInvocation = new InvocationInfo(m, getPC());
                invocationInfo.add(lastInvocation);
                
                }
            
        }
        if (seen == PUTSTATIC && getClassConstantOperand().equals(getClassName())) {
               XField f = getXFieldOperand();
               if (prev != null)
                   prev.field = f;
               if (staticFieldsReadInAnyConstructor.contains(f) && !warningGiven.contains(f)) {
                   for(InvocationInfo i : invocationInfo) {
                       Set<XField> fields = staticFieldsRead.get(i.constructor);
                       if (fields != null && fields.contains(f)) {
                           warningGiven.add(f);
                           BugInstance bug = new BugInstance(this, "SI_INSTANCE_BEFORE_FINALS_ASSIGNED", NORMAL_PRIORITY).addClassAndMethod(this);
                           if (i.field != null) {
                               bug.addField(i.field).describe(FieldAnnotation.STORED_ROLE);
                           }
                           bug.addMethod(i.constructor).describe(MethodAnnotation.METHOD_CONSTRUCTOR);
                           bug.addReferencedField(this).describe(FieldAnnotation.VALUE_OF_ROLE).addSourceLine(this, i.pc);
                           bugReporter.reportBug(bug);
                           break;
                           
                       }
                   }
               }
              
        } else if (seen == PUTSTATIC || seen == GETSTATIC || seen == INVOKESTATIC || seen == NEW)
            if (getPC() + 6 < codeBytes.length)
                requires.add(getDottedClassConstantOperand());
    }

    public void compute() {
        Set<String> allClasses = classRequires.keySet();
        Set<String> emptyClasses = new TreeSet<String>();
        for (String c : allClasses) {
            Set<String> needs = classRequires.get(c);
            needs.retainAll(allClasses);
            Set<String> extra = new TreeSet<String>();
            for (String need : needs)
                extra.addAll(classRequires.get(need));
            needs.addAll(extra);
            needs.retainAll(allClasses);
            classRequires.put(c, needs);
            if (needs.isEmpty())
                emptyClasses.add(c);
        }
        for (String c : emptyClasses) {
            classRequires.remove(c);
        }
    }

    @Override
    public void report() {

        if (DEBUG)
            System.out.println("Finishing computation");
        compute();
        compute();
        compute();
        compute();
        compute();
        compute();
        compute();
        compute();
        Set<String> allClasses = classRequires.keySet();

        for (String c : allClasses) {
            if (DEBUG)
                System.out.println("Class " + c + " requires:");
            for (String needs : (classRequires.get(c))) {
                if (DEBUG)
                    System.out.println("  " + needs);
                Set<String> s = classRequires.get(needs);
                if (s != null && s.contains(c) && c.compareTo(needs) < 0)
                    bugReporter.reportBug(new BugInstance(this, "IC_INIT_CIRCULARITY", NORMAL_PRIORITY).addClass(c).addClass(
                            needs));
            }
        }
    }

}
