/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.cf.code;

import com.android.dx.rop.cst.Constant;
import com.android.dx.rop.cst.CstMemberRef;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.Type;
import com.android.dx.util.Bits;
import com.android.dx.util.IntList;
import java.util.ArrayList;

/**
 * Utility that identifies basic blocks in bytecode.
 */
public final class BasicBlocker implements BytecodeArray.Visitor {
    /** {@code non-null;} method being converted */
    private final ConcreteMethod method;

    /**
     * {@code non-null;} work set; bits indicate offsets in need of
     * examination
     */
    private final int[] workSet;

    /**
     * {@code non-null;} live set; bits indicate potentially-live
     * opcodes; contrawise, a bit that isn't on is either in the
     * middle of an instruction or is a definitely-dead opcode
     */
    private final int[] liveSet;

    /**
     * {@code non-null;} block start set; bits indicate the starts of
     * basic blocks, including the opcodes that start blocks of
     * definitely-dead code
     */
    private final int[] blockSet;

    /**
     * {@code non-null, sparse;} for each instruction offset to a branch of
     * some sort, the list of targets for that instruction
     */
    private final IntList[] targetLists;

    /**
     * {@code non-null, sparse;} for each instruction offset to a throwing
     * instruction, the list of exception handlers for that instruction
     */
    private final ByteCatchList[] catchLists;

    /** offset of the previously parsed bytecode */
    private int previousOffset;

    /**
     * Identifies and enumerates the basic blocks in the given method,
     * returning a list of them. The returned list notably omits any
     * definitely-dead code that is identified in the process.
     *
     * @param method {@code non-null;} method to convert
     * @return {@code non-null;} list of basic blocks
     */
    public static ByteBlockList identifyBlocks(ConcreteMethod method) {
        BasicBlocker bb = new BasicBlocker(method);

        bb.doit();
        return bb.getBlockList();
    }

    /**
     * Constructs an instance. This class is not publicly instantiable; use
     * {@link #identifyBlocks}.
     *
     * @param method {@code non-null;} method to convert
     */
    private BasicBlocker(ConcreteMethod method) {
        if (method == null) {
            throw new NullPointerException("method == null");
        }

        this.method = method;

        /*
         * The "+1" below is so the idx-past-end is also valid,
         * avoiding a special case, but without preventing
         * flow-of-control falling past the end of the method from
         * getting properly reported.
         */
        int sz = method.getCode().size() + 1;

        workSet = Bits.makeBitSet(sz);
        liveSet = Bits.makeBitSet(sz);
        blockSet = Bits.makeBitSet(sz);
        targetLists = new IntList[sz];
        catchLists = new ByteCatchList[sz];
        previousOffset = -1;
    }

    /*
     * Note: These methods are defined implementation of the interface
     * BytecodeArray.Visitor; since the class isn't publicly
     * instantiable, no external code ever gets a chance to actually
     * call these methods.
     */

    /** {@inheritDoc} */
    public void visitInvalid(int opcode, int offset, int length) {
        visitCommon(offset, length, true);
    }

    /** {@inheritDoc} */
    public void visitNoArgs(int opcode, int offset, int length, Type type) {
        switch (opcode) {
            case ByteOps.IRETURN:
            case ByteOps.RETURN: {
                visitCommon(offset, length, false);
                targetLists[offset] = IntList.EMPTY;
                break;
            }
            case ByteOps.ATHROW: {
                visitCommon(offset, length, false);
                visitThrowing(offset, length, false);
                break;
            }
            case ByteOps.IALOAD:
            case ByteOps.LALOAD:
            case ByteOps.FALOAD:
            case ByteOps.DALOAD:
            case ByteOps.AALOAD:
            case ByteOps.BALOAD:
            case ByteOps.CALOAD:
            case ByteOps.SALOAD:
            case ByteOps.IASTORE:
            case ByteOps.LASTORE:
            case ByteOps.FASTORE:
            case ByteOps.DASTORE:
            case ByteOps.AASTORE:
            case ByteOps.BASTORE:
            case ByteOps.CASTORE:
            case ByteOps.SASTORE:
            case ByteOps.ARRAYLENGTH:
            case ByteOps.MONITORENTER:
            case ByteOps.MONITOREXIT: {
                /*
                 * These instructions can all throw, so they have to end
                 * the block they appear in (since throws are branches).
                 */
                visitCommon(offset, length, true);
                visitThrowing(offset, length, true);
                break;
            }
            case ByteOps.IDIV:
            case ByteOps.IREM: {
                /*
                 * The int and long versions of division and remainder may
                 * throw, but not the other types.
                 */
                visitCommon(offset, length, true);
                if ((type == Type.INT) || (type == Type.LONG)) {
                    visitThrowing(offset, length, true);
                }
                break;
            }
            default: {
                visitCommon(offset, length, true);
                break;
            }
        }
    }

    /** {@inheritDoc} */
    public void visitLocal(int opcode, int offset, int length,
            int idx, Type type, int value) {
        if (opcode == ByteOps.RET) {
            visitCommon(offset, length, false);
            targetLists[offset] = IntList.EMPTY;
        } else {
            visitCommon(offset, length, true);
        }
    }

    /** {@inheritDoc} */
    public void visitConstant(int opcode, int offset, int length,
            Constant cst, int value) {
        visitCommon(offset, length, true);

        if ((cst instanceof CstMemberRef) || (cst instanceof CstType) ||
            (cst instanceof CstString)) {
            /*
             * Instructions with these sorts of constants have the
             * possibility of throwing, so this instruction needs to
             * end its block (since it can throw, and possible-throws
             * are branch points).
             */
            visitThrowing(offset, length, true);
        }
    }

    /** {@inheritDoc} */
    public void visitBranch(int opcode, int offset, int length,
            int target) {
        switch (opcode) {
            case ByteOps.GOTO: {
                visitCommon(offset, length, false);
                targetLists[offset] = IntList.makeImmutable(target);
                break;
            }
            case ByteOps.JSR: {
                /*
                 * Each jsr is quarantined into a separate block (containing
                 * only the jsr instruction) but is otherwise treated
                 * as a conditional branch. (That is to say, both its
                 * target and next instruction begin new blocks.)
                 */
                addWorkIfNecessary(offset, true);
                // Fall through to next case...
            }
            default: {
                int next = offset + length;
                visitCommon(offset, length, true);
                addWorkIfNecessary(next, true);
                targetLists[offset] = IntList.makeImmutable(next, target);
                break;
            }
        }

        addWorkIfNecessary(target, true);
    }

    /** {@inheritDoc} */
    public void visitSwitch(int opcode, int offset, int length,
            SwitchList cases, int padding) {
        visitCommon(offset, length, false);
        addWorkIfNecessary(cases.getDefaultTarget(), true);

        int sz = cases.size();
        for (int i = 0; i < sz; i++) {
            addWorkIfNecessary(cases.getTarget(i), true);
        }

        targetLists[offset] = cases.getTargets();
    }

    /** {@inheritDoc} */
    public void visitNewarray(int offset, int length, CstType type,
            ArrayList<Constant> intVals) {
        visitCommon(offset, length, true);
        visitThrowing(offset, length, true);
    }

    /**
     * Extracts the list of basic blocks from the bit sets.
     *
     * @return {@code non-null;} the list of basic blocks
     */
    private ByteBlockList getBlockList() {
        BytecodeArray bytes = method.getCode();
        ByteBlock[] bbs = new ByteBlock[bytes.size()];
        int count = 0;

        for (int at = 0, next; /*at*/; at = next) {
            next = Bits.findFirst(blockSet, at + 1);
            if (next < 0) {
                break;
            }

            if (Bits.get(liveSet, at)) {
                /*
                 * Search backward for the branch or throwing
                 * instruction at the end of this block, if any. If
                 * there isn't any, then "next" is the sole target.
                 */
                IntList targets = null;
                int targetsAt = -1;
                ByteCatchList blockCatches;

                for (int i = next - 1; i >= at; i--) {
                    targets = targetLists[i];
                    if (targets != null) {
                        targetsAt = i;
                        break;
                    }
                }

                if (targets == null) {
                    targets = IntList.makeImmutable(next);
                    blockCatches = ByteCatchList.EMPTY;
                } else {
                    blockCatches = catchLists[targetsAt];
                    if (blockCatches == null) {
                        blockCatches = ByteCatchList.EMPTY;
                    }
                }

                bbs[count] =
                    new ByteBlock(at, at, next, targets, blockCatches);
                count++;
            }
        }

        ByteBlockList result = new ByteBlockList(count);
        for (int i = 0; i < count; i++) {
            result.set(i, bbs[i]);
        }

        return result;
    }

    /**
     * Does basic block identification.
     */
    private void doit() {
        BytecodeArray bytes = method.getCode();
        ByteCatchList catches = method.getCatches();
        int catchSz = catches.size();

        /*
         * Start by setting offset 0 as the start of a block and in need
         * of work...
         */
        Bits.set(workSet, 0);
        Bits.set(blockSet, 0);

        /*
         * And then process the work set, add new work based on
         * exception ranges that are active, and iterate until there's
         * nothing left to work on.
         */
        while (!Bits.isEmpty(workSet)) {
            try {
                bytes.processWorkSet(workSet, this);
            } catch (IllegalArgumentException ex) {
                // Translate the exception.
                throw new SimException("flow of control falls off " +
                                       "end of method",
                                       ex);
            }

            for (int i = 0; i < catchSz; i++) {
                ByteCatchList.Item item = catches.get(i);
                int start = item.getStartPc();
                int end = item.getEndPc();
                if (Bits.anyInRange(liveSet, start, end)) {
                    Bits.set(blockSet, start);
                    Bits.set(blockSet, end);
                    addWorkIfNecessary(item.getHandlerPc(), true);
                }
            }
        }
    }

    /**
     * Sets a bit in the work set, but only if the instruction in question
     * isn't yet known to be possibly-live.
     *
     * @param offset offset to the instruction in question
     * @param blockStart {@code true} iff this instruction starts a
     * basic block
     */
    private void addWorkIfNecessary(int offset, boolean blockStart) {
        if (!Bits.get(liveSet, offset)) {
            Bits.set(workSet, offset);
        }

        if (blockStart) {
            Bits.set(blockSet, offset);
        }
    }

    /**
     * Helper method used by all the visitor methods.
     *
     * @param offset offset to the instruction
     * @param length length of the instruction, in bytes
     * @param nextIsLive {@code true} iff the instruction after
     * the indicated one is possibly-live (because this one isn't an
     * unconditional branch, a return, or a switch)
     */
    private void visitCommon(int offset, int length, boolean nextIsLive) {
        Bits.set(liveSet, offset);

        if (nextIsLive) {
            /*
             * If the next instruction is flowed to by this one, just
             * add it to the work set, and then a subsequent visit*()
             * will deal with it as appropriate.
             */
            addWorkIfNecessary(offset + length, false);
        } else {
            /*
             * If the next instruction isn't flowed to by this one,
             * then mark it as a start of a block but *don't* add it
             * to the work set, so that in the final phase we can know
             * dead code blocks as those marked as blocks but not also marked
             * live.
             */
            Bits.set(blockSet, offset + length);
        }
    }

    /**
     * Helper method used by all the visitor methods that deal with
     * opcodes that possibly throw. This method should be called after calling
     * {@link #visitCommon}.
     *
     * @param offset offset to the instruction
     * @param length length of the instruction, in bytes
     * @param nextIsLive {@code true} iff the instruction after
     * the indicated one is possibly-live (because this one isn't an
     * unconditional throw)
     */
    private void visitThrowing(int offset, int length, boolean nextIsLive) {
        int next = offset + length;

        if (nextIsLive) {
            addWorkIfNecessary(next, true);
        }

        ByteCatchList catches = method.getCatches().listFor(offset);
        catchLists[offset] = catches;
        targetLists[offset] = catches.toTargetList(nextIsLive ? next : -1);
    }

    /**
     * {@inheritDoc}
     */
    public void setPreviousOffset(int offset) {
        previousOffset = offset;
    }

    /**
     * {@inheritDoc}
     */
    public int getPreviousOffset() {
        return previousOffset;
    }
}
