/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2005 Dave Brosius <dbrosius@users.sourceforge.net>
 * Copyright (C) 2005-2006 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.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;

import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.BranchInstruction;
import org.apache.bcel.generic.GotoInstruction;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.LOOKUPSWITCH;
import org.apache.bcel.generic.ReturnInstruction;
import org.apache.bcel.generic.TABLESWITCH;
import org.apache.bcel.util.ByteSequence;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.BasicBlock;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.Edge;
import edu.umd.cs.findbugs.ba.EdgeTypes;
import edu.umd.cs.findbugs.ba.MethodUnprofitableException;
import edu.umd.cs.findbugs.visitclass.PreorderVisitor;

/**
 * @author Dave Brousius 4/2005 original author
 * @author Brian Cole 7/2006 serious reworking
 */
public class DuplicateBranches extends PreorderVisitor implements Detector {
    private ClassContext classContext;

    private final BugReporter bugReporter;

    private final Collection<BugInstance> pendingBugs = new LinkedList<BugInstance>();

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

    @Override
    public void visitClassContext(ClassContext classContext) {
        this.classContext = classContext;
        classContext.getJavaClass().accept(this);
    }

    @Override
    public void visitMethod(Method method) {
        try {
            if (method.getCode() == null) {
                return;
            }

            CFG cfg = classContext.getCFG(method);

            Iterator<BasicBlock> bbi = cfg.blockIterator();
            while (bbi.hasNext()) {
                BasicBlock bb = bbi.next();

                int numOutgoing = cfg.getNumOutgoingEdges(bb);
                if (numOutgoing == 2) {
                    findIfElseDuplicates(cfg, method, bb);
                } else if (numOutgoing > 2) {
                    findSwitchDuplicates(cfg, method, bb);
                }
            }
        } catch (MethodUnprofitableException mue) {
            if (SystemProperties.getBoolean("unprofitable.debug")) {
                // don't
                // report
                bugReporter.logError("skipping unprofitable method in " + getClass().getName());
            }
        } catch (Exception e) {
            bugReporter.logError("Failure examining basic blocks in Duplicate Branches detector", e);
        }
        if (pendingBugs.size() <= 2) {
            for (BugInstance b : pendingBugs) {
                bugReporter.reportBug(b);
            }
        }
        pendingBugs.clear();

    }

    private void findIfElseDuplicates(CFG cfg, Method method, BasicBlock bb) {
        BasicBlock thenBB = null, elseBB = null;

        Iterator<Edge> iei = cfg.outgoingEdgeIterator(bb);
        while (iei.hasNext()) {
            Edge e = iei.next();
            if (e.getType() == EdgeTypes.IFCMP_EDGE) {
                elseBB = e.getTarget();
            } else if (e.getType() == EdgeTypes.FALL_THROUGH_EDGE) {
                thenBB = e.getTarget();
            }
        }

        if ((thenBB == null) || (elseBB == null)) {
            return;
        }
        InstructionHandle thenStartHandle = getDeepFirstInstruction(cfg, thenBB);
        InstructionHandle elseStartHandle = getDeepFirstInstruction(cfg, elseBB);
        if ((thenStartHandle == null) || (elseStartHandle == null)) {
            return;
        }

        int thenStartPos = thenStartHandle.getPosition();
        int elseStartPos = elseStartHandle.getPosition();

        InstructionHandle thenFinishIns = findThenFinish(cfg, thenBB, elseStartPos);
        int thenFinishPos = thenFinishIns.getPosition();

        if (!(thenFinishIns.getInstruction() instanceof GotoInstruction)) {
            return;
        }

        InstructionHandle elseFinishHandle = ((GotoInstruction) thenFinishIns.getInstruction()).getTarget();
        int elseFinishPos = elseFinishHandle.getPosition();

        if (thenFinishPos >= elseStartPos) {
            return;
        }

        if ((thenFinishPos - thenStartPos) != (elseFinishPos - elseStartPos)) {
            return;
        }

        if (thenFinishPos <= thenStartPos) {
            return;
        }

        byte[] thenBytes = getCodeBytes(method, thenStartPos, thenFinishPos);
        byte[] elseBytes = getCodeBytes(method, elseStartPos, elseFinishPos);

        if (!Arrays.equals(thenBytes, elseBytes)) {
            return;
        }

        // adjust elseFinishPos to be inclusive (for source line attribution)
        InstructionHandle elseLastIns = elseFinishHandle.getPrev();
        if (elseLastIns != null) {
            elseFinishPos = elseLastIns.getPosition();
        }

        pendingBugs.add(new BugInstance(this, "DB_DUPLICATE_BRANCHES", NORMAL_PRIORITY)
        .addClassAndMethod(classContext.getJavaClass(), method)
        .addSourceLineRange(classContext, this, thenStartPos, thenFinishPos)
        .addSourceLineRange(classContext, this, elseStartPos, elseFinishPos));
    }

    /**
     * Like bb.getFirstInstruction() except that if null is returned it will
     * follow the FALL_THROUGH_EDGE (if any)
     */
    private static InstructionHandle getDeepFirstInstruction(CFG cfg, BasicBlock bb) {
        InstructionHandle ih = bb.getFirstInstruction();
        if (ih != null) {
            return ih;
        }
        Iterator<Edge> iei = cfg.outgoingEdgeIterator(bb);
        while (iei.hasNext()) {
            Edge e = iei.next();
            String edgeString = e.toString();
            if (EdgeTypes.FALL_THROUGH_EDGE == e.getType()) {
                return getDeepFirstInstruction(cfg, e.getTarget());
            }
        }
        return null;
    }

    private void findSwitchDuplicates(CFG cfg, Method method, BasicBlock bb) {

        int[] switchPos = new int[cfg.getNumOutgoingEdges(bb) + 1];
        HashMap<Integer, InstructionHandle> prevHandle = new HashMap<Integer, InstructionHandle>();

        Iterator<Edge> iei = cfg.outgoingEdgeIterator(bb);
        int idx = 0;

        while (iei.hasNext()) {
            Edge e = iei.next();
            int eType = e.getType();
            if (eType == EdgeTypes.SWITCH_EDGE || eType == EdgeTypes.SWITCH_DEFAULT_EDGE) {
                BasicBlock target = e.getTarget();
                InstructionHandle firstIns = getDeepFirstInstruction(cfg, target);
                if (firstIns == null)
                {
                    continue; // give up on this edge
                }
                int firstInsPosition = firstIns.getPosition();
                switchPos[idx++] = firstInsPosition;
                InstructionHandle prevIns = firstIns.getPrev(); // prev in
                // bytecode, not
                // flow
                if (prevIns != null) {
                    prevHandle.put(firstInsPosition, prevIns);
                }
            } else {
                // hmm, this must not be a switch statement, so give up
                return;
            }
        }

        if (idx < 2) {
            return;
        }

        Arrays.sort(switchPos, 0, idx); // sort the 'idx' switch positions

        // compute end position of final case (ok if set to 0 or <=
        // switchPos[idx-1])
        switchPos[idx] = getFinalTarget(cfg, switchPos[idx - 1], prevHandle.values());

        HashMap<BigInteger, Collection<Integer>> map = new HashMap<BigInteger, Collection<Integer>>();
        for (int i = 0; i < idx; i++) {
            if (switchPos[i] + 7 >= switchPos[i + 1])
            {
                continue; // ignore small switch clauses
            }

            int endPos = switchPos[i + 1];
            InstructionHandle last = prevHandle.get(switchPos[i + 1]);
            if (last == null) {
                // should be default case -- leave endPos as is
            } else if (last.getInstruction() instanceof GotoInstruction) {
                endPos = last.getPosition(); // don't store the goto
            } else if (last.getInstruction() instanceof ReturnInstruction) {
                // leave endPos as is (store the return instruction)
                // } else if (last.getInstruction() instanceof ATHROW) {
                // // leave endPos as is (store the throw instruction)
                // Don't do this since many cases may throw "not implemented".
            } else {
                if (i + 2 < idx)
                {
                    continue; // falls through to next case, so don't store it
                }
                // at all
                if (i + 1 < idx && switchPos[idx] != switchPos[idx - 1])
                {
                    continue; // also falls through unless switch has no default
                    // case
                }
            }

            BigInteger clauseAsInt = getCodeBytesAsBigInt(method, switchPos, i, endPos);
            updateMap(map, i, clauseAsInt);

        }
        for (Collection<Integer> clauses : map.values()) {
            if (clauses.size() > 1) {
                BugInstance bug = new BugInstance(this, "DB_DUPLICATE_SWITCH_CLAUSES", LOW_PRIORITY).addClassAndMethod(
                        classContext.getJavaClass(), method);
                for (int i : clauses)
                {
                    bug.addSourceLineRange(this.classContext, this, switchPos[i], switchPos[i + 1] - 1); // not
                }
                // endPos,
                // but
                // that's
                // ok
                pendingBugs.add(bug);
            }
        }
    }

    private void updateMap(HashMap<BigInteger, Collection<Integer>> map, int i, BigInteger clauseAsInt) {
        Collection<Integer> values = map.get(clauseAsInt);

        if (values == null) {
            values = new LinkedList<Integer>();
            map.put(clauseAsInt, values);
        }
        values.add(i); // index into the sorted array
    }

    private BigInteger getCodeBytesAsBigInt(Method method, int[] switchPos, int i, int endPos) {
        byte[] clause = getCodeBytes(method, switchPos[i], endPos);

        BigInteger clauseAsInt;
        if (clause.length == 0) {
            clauseAsInt = BigInteger.ZERO;
        } else {
            clauseAsInt = new BigInteger(clause);
        }
        return clauseAsInt;
    }

    /**
     * determine the end position (exclusive) of the final case by looking at
     * the gotos at the ends of the other cases
     */
    private static int getFinalTarget(CFG cfg, int myPos, Collection<InstructionHandle> prevs) {
        int maxGoto = 0;
        BasicBlock myBB = null;
        // note: InstructionHandle doesn't override equals(), so use
        // prevs.contains() with caution.
        Iterator<BasicBlock> bbi = cfg.blockIterator();
        while (bbi.hasNext()) {
            BasicBlock bb = bbi.next();
            InstructionHandle last = bb.getLastInstruction(); // may be null
            if (prevs.contains(last)) { // danger will robinson
                Iterator<Edge> iei = cfg.outgoingEdgeIterator(bb);
                while (iei.hasNext()) {
                    Edge e = iei.next();
                    int eType = e.getType();
                    String aab = e.toString();
                    if (eType == EdgeTypes.GOTO_EDGE) {
                        BasicBlock target = e.getTarget();
                        InstructionHandle targetFirst = getDeepFirstInstruction(cfg, target);
                        if (targetFirst != null) {
                            int targetPos = targetFirst.getPosition();
                            if (targetPos > maxGoto) {
                                maxGoto = targetPos;
                            }
                        }
                    }
                }
            } else if (last != null && myPos == bb.getFirstInstruction().getPosition()) {
                // note: getFirstInstruction() may return null, but won't if
                // last!=null.
                myBB = bb; // used in case (c) below
            }
        }
        /*
         * ok, there are three cases: a) maxGoto == myPos: There is no default
         * case within the switch. b) maxGoto > myPos: maxGoto is the end
         * (exclusive) of the default case c) maxGoto < myPos: all the cases do
         * something like return or throw, so who knows if there is a default
         * case (and it's length)?
         */
        if (maxGoto < myPos && myBB != null) {
            /*
             * We're in case (c), so let's guess that the end of the basic block
             * is the end of the default case (if it exists). This may give
             * false negatives (if the default case has branches, for example)
             * but shouldn't give false negatives (because if it matches one of
             * the cases, then it has also matched that case's return/throw).
             */
            InstructionHandle last = myBB.getLastInstruction();
            if (last != null) {
                // note: last.getNext() may return null, so do it this way
                return last.getPosition() + last.getInstruction().getLength();
            }
        }
        return maxGoto;
    }

    private byte[] getCodeBytes(Method m, int start, int end) {
        byte[] code = m.getCode().getCode();
        byte[] bytes = new byte[end - start];
        System.arraycopy(code, start, bytes, 0, end - start);

        try {
            ByteSequence sequence = new ByteSequence(code);
            while ((sequence.available() > 0) && (sequence.getIndex() < start)) {
                Instruction.readInstruction(sequence);
            }

            int pos;
            while (sequence.available() > 0 && ((pos = sequence.getIndex()) < end)) {
                Instruction ins = Instruction.readInstruction(sequence);
                if ((ins instanceof BranchInstruction) && !(ins instanceof TABLESWITCH) && !(ins instanceof LOOKUPSWITCH)) {
                    BranchInstruction bi = (BranchInstruction) ins;
                    int offset = bi.getIndex();
                    int target = offset + pos;
                    if (target >= end) { // or target < start ??
                        byte hiByte = (byte) ((target >> 8) & 0x000000FF);
                        byte loByte = (byte) (target & 0x000000FF);
                        bytes[pos + bi.getLength() - 2 - start] = hiByte;
                        bytes[pos + bi.getLength() - 1 - start] = loByte;
                    }
                }
            }
        } catch (IOException ioe) {
        }

        return bytes;
    }

    private InstructionHandle findThenFinish(CFG cfg, BasicBlock thenBB, int elsePos) {
        InstructionHandle inst = thenBB.getFirstInstruction();
        while (inst == null) {
            Iterator<Edge> ie = cfg.outgoingEdgeIterator(thenBB);
            while (ie.hasNext()) {
                Edge e = ie.next();
                if (e.getType() == EdgeTypes.FALL_THROUGH_EDGE) {
                    thenBB = e.getTarget();
                    break;
                }
            }
            inst = thenBB.getFirstInstruction();
        }

        InstructionHandle lastIns = inst;
        while (inst.getPosition() < elsePos) {
            lastIns = inst;
            inst = inst.getNext();
        }

        return lastIns;
    }

    @Override
    public void report() {
    }
}
