/*
 * Copyright (C) 2012 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.tools.lint.checks;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

//import org.objectweb.asm.util.Printer;

/**
 * A {@linkplain ControlFlowGraph} is a graph containing a node for each
 * instruction in a method, and an edge for each possible control flow; usually
 * just "next" for the instruction following the current instruction, but in the
 * case of a branch such as an "if", multiple edges to each successive location,
 * or with a "goto", a single edge to the jumped-to instruction.
 * <p>
 * It also adds edges for abnormal control flow, such as the possibility of a
 * method call throwing a runtime exception.
 */
public class ControlFlowGraph {
    /** Map from instructions to nodes */
    private Map<AbstractInsnNode, Node> mNodeMap;
    private MethodNode mMethod;

    /**
     * Creates a new {@link ControlFlowGraph} and populates it with the flow
     * control for the given method. If the optional {@code initial} parameter is
     * provided with an existing graph, then the graph is simply populated, not
     * created. This allows subclassing of the graph instance, if necessary.
     *
     * @param initial usually null, but can point to an existing instance of a
     *            {@link ControlFlowGraph} in which that graph is reused (but
     *            populated with new edges)
     * @param classNode the class containing the method to be analyzed
     * @param method the method to be analyzed
     * @return a {@link ControlFlowGraph} with nodes for the control flow in the
     *         given method
     * @throws AnalyzerException if the underlying bytecode library is unable to
     *             analyze the method bytecode
     */
    @NonNull
    public static ControlFlowGraph create(
            @Nullable ControlFlowGraph initial,
            @NonNull ClassNode classNode,
            @NonNull MethodNode method) throws AnalyzerException {
        final ControlFlowGraph graph = initial != null ? initial : new ControlFlowGraph();
        final InsnList instructions = method.instructions;
        graph.mNodeMap = Maps.newHashMapWithExpectedSize(instructions.size());
        graph.mMethod = method;

        // Create a flow control graph using ASM5's analyzer. According to the ASM 4 guide
        // (download.forge.objectweb.org/asm/asm4-guide.pdf) there are faster ways to construct
        // it, but those require a lot more code.
        Analyzer analyzer = new Analyzer(new BasicInterpreter()) {
            @Override
            protected void newControlFlowEdge(int insn, int successor) {
                // Update the information as of whether the this object has been
                // initialized at the given instruction.
                AbstractInsnNode from = instructions.get(insn);
                AbstractInsnNode to = instructions.get(successor);
                graph.add(from, to);
            }

            @Override
            protected boolean newControlFlowExceptionEdge(int insn, TryCatchBlockNode tcb) {
                AbstractInsnNode from = instructions.get(insn);
                graph.exception(from, tcb);
                return super.newControlFlowExceptionEdge(insn, tcb);
            }

            @Override
            protected boolean newControlFlowExceptionEdge(int insn, int successor) {
                AbstractInsnNode from = instructions.get(insn);
                AbstractInsnNode to = instructions.get(successor);
                graph.exception(from, to);
                return super.newControlFlowExceptionEdge(insn, successor);
            }
        };

        analyzer.analyze(classNode.name, method);
        return graph;
    }

    /**
     * Checks whether there is a path from the given source node to the given
     * destination node
     */
    @SuppressWarnings("MethodMayBeStatic")
    private boolean isConnected(@NonNull Node from,
            @NonNull Node to, @NonNull Set<Node> seen) {
        if (from == to) {
            return true;
        } else if (seen.contains(from)) {
            return false;
        }
        seen.add(from);

        List<Node> successors = from.successors;
        List<Node> exceptions = from.exceptions;
        if (exceptions != null) {
            for (Node successor : exceptions) {
                if (isConnected(successor, to, seen)) {
                    return true;
                }
            }
        }

        if (successors != null) {
            for (Node successor : successors) {
                if (isConnected(successor, to, seen)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Checks whether there is a path from the given source node to the given
     * destination node
     */
    public boolean isConnected(@NonNull Node from, @NonNull Node to) {
        return isConnected(from, to, Sets.<Node>newIdentityHashSet());
    }

    /**
     * Checks whether there is a path from the given instruction to the given
     * instruction node
     */
    public boolean isConnected(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
        return isConnected(getNode(from), getNode(to));
    }

    /** A {@link Node} is a node in the control flow graph for a method, pointing to
     * the instruction and its possible successors */
    public static class Node {
        /** The instruction */
        public final AbstractInsnNode instruction;
        /** Any normal successors (e.g. following instruction, or goto or conditional flow) */
        public final List<Node> successors = new ArrayList<Node>(2);
        /** Any abnormal successors (e.g. the handler to go to following an exception) */
        public final List<Node> exceptions = new ArrayList<Node>(1);

        /** A tag for use during depth-first-search iteration of the graph etc */
        public int visit;

        /**
         * Constructs a new control graph node
         *
         * @param instruction the instruction to associate with this node
         */
        public Node(@NonNull AbstractInsnNode instruction) {
            this.instruction = instruction;
        }

        void addSuccessor(@NonNull Node node) {
            if (!successors.contains(node)) {
                successors.add(node);
            }
        }

        void addExceptionPath(@NonNull Node node) {
            if (!exceptions.contains(node)) {
                exceptions.add(node);
            }
        }

        /**
         * Represents this instruction as a string, for debugging purposes
         *
         * @param includeAdjacent whether it should include a display of
         *            adjacent nodes as well
         * @return a string representation
         */
        @NonNull
        public String toString(boolean includeAdjacent) {
            StringBuilder sb = new StringBuilder(100);

            sb.append(getId(instruction));
            sb.append(':');

            if (instruction instanceof LabelNode) {
                //LabelNode l = (LabelNode) instruction;
                //sb.append('L' + l.getLabel().getOffset() + ":");
                //sb.append('L' + l.getLabel().info + ":");
                sb.append("LABEL");
            } else if (instruction instanceof LineNumberNode) {
                sb.append("LINENUMBER ").append(((LineNumberNode)instruction).line);
            } else if (instruction instanceof FrameNode) {
                sb.append("FRAME");
            } else {
                int opcode = instruction.getOpcode();
                String opcodeName = getOpcodeName(opcode);
                sb.append(opcodeName);
                if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
                    sb.append('(').append(((MethodInsnNode)instruction).name).append(')');
                }
            }

            if (includeAdjacent) {
                if (successors != null && !successors.isEmpty()) {
                    sb.append(" Next:");
                    for (Node successor : successors) {
                        sb.append(' ');
                        sb.append(successor.toString(false));
                    }
                }

                if (exceptions != null && !exceptions.isEmpty()) {
                    sb.append(" Exceptions:");
                    for (Node exception : exceptions) {
                        sb.append(' ');
                        sb.append(exception.toString(false));
                    }
                }
                sb.append('\n');
            }

            return sb.toString();
        }
    }

    /** Adds an exception flow to this graph */
    protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
        getNode(from).addSuccessor(getNode(to));
    }

    /** Adds an exception flow to this graph */
    protected void exception(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
        // For now, these edges appear useless; we also get more specific
        // information via the TryCatchBlockNode which we use instead.
        //getNode(from).addExceptionPath(getNode(to));
    }

    /** Adds an exception try block node to this graph */
    protected void exception(@NonNull AbstractInsnNode from, @NonNull TryCatchBlockNode tcb) {
        // Add tcb's to all instructions in the range
        LabelNode start = tcb.start;
        LabelNode end = tcb.end; // exclusive

        // Add exception edges for all method calls in the range
        AbstractInsnNode curr = start;
        Node handlerNode = getNode(tcb.handler);
        while (curr != end && curr != null) {
            // A method can throw can exception, or a throw instruction directly
            if (curr.getType() == AbstractInsnNode.METHOD_INSN
                    || (curr.getType() == AbstractInsnNode.INSN
                    && curr.getOpcode() == Opcodes.ATHROW)) {
                // Method call; add exception edge to handler
                if (tcb.type == null) {
                    // finally block: not an exception path
                    getNode(curr).addSuccessor(handlerNode);
                }
                getNode(curr).addExceptionPath(handlerNode);
            }
            curr = curr.getNext();
        }
    }

    /**
     * Looks up (and if necessary) creates a graph node for the given instruction
     *
     * @param instruction the instruction
     * @return the control flow graph node corresponding to the given
     *         instruction
     */
    @NonNull
    public Node getNode(@NonNull AbstractInsnNode instruction) {
        Node node = mNodeMap.get(instruction);
        if (node == null) {
            node = new Node(instruction);
            mNodeMap.put(instruction, node);
        }

        return node;
    }

    /**
     * Creates a human readable version of the graph
     *
     * @param start the starting instruction, or null if not known or to use the
     *            first instruction
     * @return a string version of the graph
     */
    @NonNull
    public String toString(@Nullable Node start) {
        StringBuilder sb = new StringBuilder(400);

        AbstractInsnNode curr;
        if (start != null) {
            curr = start.instruction;
        } else {
            if (mNodeMap.isEmpty()) {
                return "<empty>";
            } else {
                curr = mNodeMap.keySet().iterator().next();
                while (curr.getPrevious() != null) {
                    curr = curr.getPrevious();
                }
            }
        }

        while (curr != null) {
            Node node = mNodeMap.get(curr);
            if (node != null) {
                sb.append(node.toString(true));
            }
            curr = curr.getNext();
        }

        return sb.toString();
    }

    @Override
    public String toString() {
        return toString(null);
    }

    // ---- For debugging only ----

    private static Map<Object, String> sIds = null;
    private static int sNextId = 1;
    private static String getId(Object object) {
        if (sIds == null) {
            sIds = Maps.newHashMap();
        }
        String id = sIds.get(object);
        if (id == null) {
            id = Integer.toString(sNextId++);
            sIds.put(object, id);
        }
        return id;
    }

    /**
     * Generates dot output of the graph. This can be used with
     * graphwiz to visualize the graph. For example, if you
     * save the output as graph1.gv you can run
     * <pre>
     * $ dot -Tps graph1.gv -o graph1.ps
     * </pre>
     * to generate a postscript file, which you can then view
     * with "gv graph1.ps".
     *
     * (There are also some online web sites where you can
     * paste in dot graphs and see the visualization right
     * there in the browser.)
     *
     * @return a dot description of this control flow graph,
     *    useful for debugging
     */
    @SuppressWarnings("unused")
    public String toDot(@Nullable Set<Node> highlight) {
        StringBuilder sb = new StringBuilder();
        sb.append("digraph G {\n");


        AbstractInsnNode instruction = mMethod.instructions.getFirst();

        // Special start node
        sb.append("  start -> ").append(getId(mNodeMap.get(instruction))).append(";\n");
        sb.append("  start [shape=plaintext];\n");

        while (instruction != null) {
            Node node = mNodeMap.get(instruction);
            if (node != null) {
                if (node.successors != null) {
                    for (Node to : node.successors) {
                        sb.append("  ").append(getId(node)).append(" -> ").append(getId(to));
                        if (node.instruction instanceof JumpInsnNode) {
                            sb.append(" [label=\"");
                            if (((JumpInsnNode)node.instruction).label == to.instruction) {
                                sb.append("yes");
                            } else {
                                sb.append("no");
                            }
                            sb.append("\"]");
                        }
                        sb.append(";\n");
                    }
                }
                if (node.exceptions != null) {
                    for (Node to : node.exceptions) {
                        sb.append(getId(node)).append(" -> ").append(getId(to));
                        sb.append(" [label=\"exception\"];\n");
                    }
                }
            }

            instruction = instruction.getNext();
        }


        // Labels
        sb.append("\n");
        for (Node node : mNodeMap.values()) {
            instruction = node.instruction;
            sb.append("  ").append(getId(node)).append(" ");
            sb.append("[label=\"").append(dotDescribe(node)).append("\"");
            if (highlight != null && highlight.contains(node)) {
                sb.append(",shape=box,style=filled");
            } else if (instruction instanceof LineNumberNode ||
              instruction instanceof LabelNode ||
              instruction instanceof FrameNode) {
                sb.append(",shape=oval,style=dotted");
            } else {
                sb.append(",shape=box");
            }
            sb.append("];\n");
        }

        sb.append("}");
        return sb.toString();
    }

    private static String dotDescribe(Node node) {
        AbstractInsnNode instruction = node.instruction;
        if (instruction instanceof LabelNode) {
            return "Label";
        } else if (instruction instanceof LineNumberNode) {
            LineNumberNode lineNode = (LineNumberNode)instruction;
            return "Line " + lineNode.line;
        } else if (instruction instanceof FrameNode) {
            return "Stack Frame";
        } else if (instruction instanceof MethodInsnNode) {
            MethodInsnNode method = (MethodInsnNode)instruction;
            String cls = method.owner.substring(method.owner.lastIndexOf('/') + 1);
            cls = cls.replace('$','.');
            return "Call " + cls + "#" + method.name;
        } else if (instruction instanceof FieldInsnNode) {
            FieldInsnNode field = (FieldInsnNode) instruction;
            String cls = field.owner.substring(field.owner.lastIndexOf('/') + 1);
            cls = cls.replace('$','.');
            return "Field " + cls + "#" + field.name;
        } else if (instruction instanceof TypeInsnNode && instruction.getOpcode() == Opcodes.NEW) {
            return "New " + ((TypeInsnNode)instruction).desc;
        }
        StringBuilder sb = new StringBuilder();
        String opcodeName = getOpcodeName(instruction.getOpcode());
        sb.append(opcodeName);

        if (instruction instanceof IntInsnNode) {
            IntInsnNode in = (IntInsnNode) instruction;
            sb.append(" ").append(Integer.toString(in.operand));
        } else if (instruction instanceof LdcInsnNode) {
            LdcInsnNode ldc = (LdcInsnNode) instruction;
            sb.append(" ");
            if (ldc.cst instanceof String) {
                sb.append("\\\"");
            }
            sb.append(ldc.cst);
            if (ldc.cst instanceof String) {
                sb.append("\\\"");
            }
        }
        return sb.toString();
    }

    private static String getOpcodeName(int opcode) {
        if (sOpcodeNames == null) {
            sOpcodeNames = new String[255];
            try {
                Field[] fields = Opcodes.class.getDeclaredFields();
                for (Field field : fields) {
                    if (field.getType() == int.class) {
                        String name = field.getName();
                        if (name.startsWith("ASM") || name.startsWith("V1_") ||
                            name.startsWith("ACC_") || name.startsWith("T_") ||
                            name.startsWith("H_") || name.startsWith("F_")) {
                            continue;
                        }
                        int val = field.getInt(null);
                        if (val >= 0 && val < sOpcodeNames.length) {
                            sOpcodeNames[val] = field.getName();
                        }

                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (opcode >= 0 && opcode < sOpcodeNames.length) {
            String name = sOpcodeNames[opcode];
            if (name != null) {
                return name;
            }
        }

        return Integer.toString(opcode);
    }

    private static String[] sOpcodeNames;
}

