/*
 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.ir.debug;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jdk.internal.org.objectweb.asm.Attribute;
import jdk.internal.org.objectweb.asm.Handle;
import jdk.internal.org.objectweb.asm.Label;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.signature.SignatureReader;
import jdk.internal.org.objectweb.asm.util.Printer;
import jdk.internal.org.objectweb.asm.util.TraceSignatureVisitor;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.linker.Bootstrap;
import jdk.nashorn.internal.runtime.linker.NameCodec;
import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;

/**
 * Pretty printer for --print-code.
 * Also supports dot formats if --print-code has arguments
 */
public final class NashornTextifier extends Printer {
    private static final String BOOTSTRAP_CLASS_NAME = Bootstrap.class.getName().replace('.', '/');

    private String currentClassName;
    private Iterator<Label> labelIter;
    private Graph graph;
    private String currentBlock;

    // Following variables are used to govern the state of collapsing long sequences of NOP.
    /** True if the last instruction was a NOP. */
    private boolean lastWasNop = false;
    /** True if ellipse ("...") was emitted in place of a second NOP. */
    private boolean lastWasEllipse = false;

    private static final int INTERNAL_NAME = 0;
    private static final int FIELD_DESCRIPTOR = 1;
    private static final int FIELD_SIGNATURE = 2;
    private static final int METHOD_DESCRIPTOR = 3;
    private static final int METHOD_SIGNATURE = 4;
    private static final int CLASS_SIGNATURE = 5;

    private final String tab = "  ";
    private final String tab2 = "    ";
    private final String tab3 = "      ";

    private Map<Label, String> labelNames;

    private boolean localVarsStarted = false;

    private NashornClassReader cr;
    private ScriptEnvironment env;

    /**
     * Constructs a new {@link NashornTextifier}. <i>Subclasses must not use this
     * constructor</i>. Instead, they must use the {@link #NashornTextifier(int)}
     * version.
     * @param env script environment
     * @param cr a customized classreader for gathering, among other things, label
     * information
     */
    public NashornTextifier(final ScriptEnvironment env, final NashornClassReader cr) {
        this(Opcodes.ASM5);
        this.env = env;
        this.cr = cr;
    }

    private NashornTextifier(final ScriptEnvironment env, final NashornClassReader cr, final Iterator<Label> labelIter, final Graph graph) {
        this(env, cr);
        this.labelIter = labelIter;
        this.graph = graph;
    }

    /**
     * Constructs a new {@link NashornTextifier}.
     *
     * @param api
     *            the ASM API version implemented by this visitor. Must be one
     *            of {@link Opcodes#ASM4} or {@link Opcodes#ASM5}.
     */
    protected NashornTextifier(final int api) {
        super(api);
    }

    @Override
    public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) {
        final int major = version & 0xFFFF;
        final int minor = version >>> 16;

        currentClassName = name;

        final StringBuilder sb = new StringBuilder();
        sb.append("// class version ").
            append(major).
            append('.').
            append(minor).append(" (").
            append(version).
            append(")\n");

        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            sb.append("// DEPRECATED\n");
        }

        sb.append("// access flags 0x"). //TODO TRANSLATE TO WHAT THEY MEAN
            append(Integer.toHexString(access).toUpperCase()).
            append('\n');

        appendDescriptor(sb, CLASS_SIGNATURE, signature);
        if (signature != null) {
            final TraceSignatureVisitor sv = new TraceSignatureVisitor(access);
            final SignatureReader r = new SignatureReader(signature);
            r.accept(sv);
            sb.append("// declaration: ").
                append(name).
                append(sv.getDeclaration()).
                append('\n');
        }

        appendAccess(sb, access & ~Opcodes.ACC_SUPER);
        if ((access & Opcodes.ACC_ANNOTATION) != 0) {
            sb.append("@interface ");
        } else if ((access & Opcodes.ACC_INTERFACE) != 0) {
            sb.append("interface ");
        } else if ((access & Opcodes.ACC_ENUM) == 0) {
            sb.append("class ");
        }
        appendDescriptor(sb, INTERNAL_NAME, name);

        if (superName != null && !"java/lang/Object".equals(superName)) {
            sb.append(" extends ");
            appendDescriptor(sb, INTERNAL_NAME, superName);
            sb.append(' ');
        }
        if (interfaces != null && interfaces.length > 0) {
            sb.append(" implements ");
            for (final String interface1 : interfaces) {
                appendDescriptor(sb, INTERNAL_NAME, interface1);
                sb.append(' ');
            }
        }
        sb.append(" {\n");

        addText(sb);
    }

    @Override
    public void visitSource(final String file, final String debug) {
        final StringBuilder sb = new StringBuilder();
        if (file != null) {
            sb.append(tab).
                append("// compiled from: ").
                append(file).
                append('\n');
        }
        if (debug != null) {
            sb.append(tab).
                append("// debug info: ").
                append(debug).
                append('\n');
        }
        if (sb.length() > 0) {
            addText(sb);
        }
    }

    @Override
    public void visitOuterClass(final String owner, final String name, final String desc) {
        final StringBuilder sb = new StringBuilder();
        sb.append(tab).append("outer class ");
        appendDescriptor(sb, INTERNAL_NAME, owner);
        sb.append(' ');
        if (name != null) {
            sb.append(name).append(' ');
        }
        appendDescriptor(sb, METHOD_DESCRIPTOR, desc);
        sb.append('\n');
        addText(sb);
    }

    @Override
    public NashornTextifier visitField(final int access, final String name, final String desc, final String signature, final Object value) {
        final StringBuilder sb = new StringBuilder();
//        sb.append('\n');
        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            sb.append(tab).append("// DEPRECATED\n");
        }

/*        sb.append(tab).
            append("// access flags 0x").
            append(Integer.toHexString(access).toUpperCase()).
            append('\n');
*/

        if (signature != null) {
            sb.append(tab);
            appendDescriptor(sb, FIELD_SIGNATURE, signature);

            final TraceSignatureVisitor sv = new TraceSignatureVisitor(0);
            final SignatureReader r = new SignatureReader(signature);
            r.acceptType(sv);
            sb.append(tab).
                append("// declaration: ").
                append(sv.getDeclaration()).
                append('\n');
        }

        sb.append(tab);
        appendAccess(sb, access);

        final String prunedDesc = desc.endsWith(";") ? desc.substring(0, desc.length() - 1) : desc;
        appendDescriptor(sb, FIELD_DESCRIPTOR, prunedDesc);
        sb.append(' ').append(name);
        if (value != null) {
            sb.append(" = ");
            if (value instanceof String) {
                sb.append('\"').append(value).append('\"');
            } else {
                sb.append(value);
            }
        }

        sb.append(";\n");
        addText(sb);

        final NashornTextifier t = createNashornTextifier();
        addText(t.getText());

        return t;
    }

    @Override
    public NashornTextifier visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {

        graph = new Graph(name);

        final List<Label> extraLabels = cr.getExtraLabels(currentClassName, name, desc);
        this.labelIter = extraLabels == null ? null : extraLabels.iterator();

        final StringBuilder sb = new StringBuilder();

        sb.append('\n');
        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            sb.append(tab).
                append("// DEPRECATED\n");
        }

        sb.append(tab).
            append("// access flags 0x").
            append(Integer.toHexString(access).toUpperCase()).
            append('\n');

        if (signature != null) {
            sb.append(tab);
            appendDescriptor(sb, METHOD_SIGNATURE, signature);

            final TraceSignatureVisitor v = new TraceSignatureVisitor(0);
            final SignatureReader r = new SignatureReader(signature);
            r.accept(v);
            final String genericDecl = v.getDeclaration();
            final String genericReturn = v.getReturnType();
            final String genericExceptions = v.getExceptions();

            sb.append(tab).
                append("// declaration: ").
                append(genericReturn).
                append(' ').
                append(name).
                append(genericDecl);

            if (genericExceptions != null) {
                sb.append(" throws ").append(genericExceptions);
            }
            sb.append('\n');
        }

        sb.append(tab);
        appendAccess(sb, access);
        if ((access & Opcodes.ACC_NATIVE) != 0) {
            sb.append("native ");
        }
        if ((access & Opcodes.ACC_VARARGS) != 0) {
            sb.append("varargs ");
        }
        if ((access & Opcodes.ACC_BRIDGE) != 0) {
            sb.append("bridge ");
        }

        sb.append(name);
        appendDescriptor(sb, METHOD_DESCRIPTOR, desc);
        if (exceptions != null && exceptions.length > 0) {
            sb.append(" throws ");
            for (final String exception : exceptions) {
                appendDescriptor(sb, INTERNAL_NAME, exception);
                sb.append(' ');
            }
        }

        sb.append('\n');
        addText(sb);

        final NashornTextifier t = createNashornTextifier();
        addText(t.getText());
        return t;
    }

    @Override
    public void visitClassEnd() {
        addText("}\n");
    }

    @Override
    public void visitFieldEnd() {
        //empty
    }

    @Override
    public void visitParameter(final String name, final int access) {
        final StringBuilder sb = new StringBuilder();
        sb.append(tab2).append("// parameter ");
        appendAccess(sb, access);
        sb.append(' ').append(name == null ? "<no name>" : name)
                .append('\n');
        addText(sb);
    }

    @Override
    public void visitCode() {
        //empty
    }

    @Override
    public void visitFrame(final int type, final int nLocal, final Object[] local, final int nStack, final Object[] stack) {
        final StringBuilder sb = new StringBuilder();
        sb.append("frame ");
        switch (type) {
        case Opcodes.F_NEW:
        case Opcodes.F_FULL:
            sb.append("full [");
            appendFrameTypes(sb, nLocal, local);
            sb.append("] [");
            appendFrameTypes(sb, nStack, stack);
            sb.append(']');
            break;
        case Opcodes.F_APPEND:
            sb.append("append [");
            appendFrameTypes(sb, nLocal, local);
            sb.append(']');
            break;
        case Opcodes.F_CHOP:
            sb.append("chop ").append(nLocal);
            break;
        case Opcodes.F_SAME:
            sb.append("same");
            break;
        case Opcodes.F_SAME1:
            sb.append("same1 ");
            appendFrameTypes(sb, 1, stack);
            break;
        default:
            assert false;
            break;
        }
        sb.append('\n');
        sb.append('\n');
        addText(sb);
    }

    private StringBuilder appendOpcode(final StringBuilder sb, final int opcode) {
        final Label next = getNextLabel();
        if (next instanceof NashornLabel) {
            final int bci = next.getOffset();
            if (bci != -1) {
                final String bcis = "" + bci;
                for (int i = 0; i < 5 - bcis.length(); i++) {
                    sb.append(' ');
                }
                sb.append(bcis);
                sb.append(' ');
            } else {
                sb.append("       ");
            }
        }

        return sb.append(tab2).append(OPCODES[opcode].toLowerCase());
    }

    private Label getNextLabel() {
        return labelIter == null ? null : labelIter.next();
    }

    @Override
    public void visitInsn(final int opcode) {
        if(opcode == Opcodes.NOP) {
            if(lastWasEllipse) {
                getNextLabel();
                return;
            } else if(lastWasNop) {
                getNextLabel();
                addText("          ...\n");
                lastWasEllipse = true;
                return;
            } else {
                lastWasNop = true;
            }
        } else {
            lastWasNop = lastWasEllipse = false;
        }
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode).append('\n');
        addText(sb);
        checkNoFallThru(opcode, null);
    }

    @Override
    public void visitIntInsn(final int opcode, final int operand) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode)
                .append(' ')
                .append(opcode == Opcodes.NEWARRAY ? TYPES[operand] : Integer
                        .toString(operand)).append('\n');
        addText(sb);
    }

    @Override
    public void visitVarInsn(final int opcode, final int var) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode).append(' ').append(var).append('\n');
        addText(sb);
    }

    @Override
    public void visitTypeInsn(final int opcode, final String type) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode).append(' ');
        appendDescriptor(sb, INTERNAL_NAME, type);
        sb.append('\n');
        addText(sb);
    }

    @Override
    public void visitFieldInsn(final int opcode, final String owner, final String name, final String desc) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode).append(' ');
        appendDescriptor(sb, INTERNAL_NAME, owner);
        sb.append('.').append(name).append(" : ");
        appendDescriptor(sb, FIELD_DESCRIPTOR, desc);
        sb.append('\n');
        addText(sb);
    }

    @Override
    public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc, final boolean itf) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode).append(' ');
        appendDescriptor(sb, INTERNAL_NAME, owner);
        sb.append('.').append(name);
        appendDescriptor(sb, METHOD_DESCRIPTOR, desc);
        sb.append('\n');
        addText(sb);
    }

    @Override
    public void visitInvokeDynamicInsn(final String name, final String desc, final Handle bsm, final Object... bsmArgs) {
        final StringBuilder sb = new StringBuilder();

        appendOpcode(sb, Opcodes.INVOKEDYNAMIC).append(' ');
        final boolean isNashornBootstrap = isNashornBootstrap(bsm);
        final boolean isNashornMathBootstrap = isNashornMathBootstrap(bsm);
        if (isNashornBootstrap) {
            sb.append(NashornCallSiteDescriptor.getOperationName((Integer)bsmArgs[0]));
            final String decodedName = NameCodec.decode(name);
            if (!decodedName.isEmpty()) {
                sb.append(':').append(decodedName);
            }
        } else {
            sb.append(name);
        }
        appendDescriptor(sb, METHOD_DESCRIPTOR, desc);
        final int len = sb.length();
        for (int i = 0; i < 80 - len ; i++) {
            sb.append(' ');
        }
        sb.append(" [");
        appendHandle(sb, bsm);
        if (bsmArgs.length == 0) {
            sb.append("none");
        } else {
            for (final Object cst : bsmArgs) {
                if (cst instanceof String) {
                    appendStr(sb, (String)cst);
                } else if (cst instanceof Type) {
                    sb.append(((Type)cst).getDescriptor()).append(".class");
                } else if (cst instanceof Handle) {
                    appendHandle(sb, (Handle)cst);
                } else if (cst instanceof Integer && isNashornBootstrap) {
                    NashornCallSiteDescriptor.appendFlags((Integer) cst, sb);
                } else if (cst instanceof Integer && isNashornMathBootstrap) {
                    sb.append(" pp=").append(cst);
                } else {
                    sb.append(cst);
                }
                sb.append(", ");
            }
            sb.setLength(sb.length() - 2);
        }

        sb.append("]\n");
        addText(sb);
    }

    private static boolean isNashornBootstrap(final Handle bsm) {
        return "bootstrap".equals(bsm.getName()) && BOOTSTRAP_CLASS_NAME.equals(bsm.getOwner());
    }

    private static boolean isNashornMathBootstrap(final Handle bsm) {
        return "mathBootstrap".equals(bsm.getName()) && BOOTSTRAP_CLASS_NAME.equals(bsm.getOwner());
    }

    private static boolean noFallThru(final int opcode) {
        switch (opcode) {
        case Opcodes.GOTO:
        case Opcodes.ATHROW:
        case Opcodes.ARETURN:
        case Opcodes.IRETURN:
        case Opcodes.LRETURN:
        case Opcodes.FRETURN:
        case Opcodes.DRETURN:
            return true;
        default:
            return false;
        }
    }

    private void checkNoFallThru(final int opcode, final String to) {
        if (noFallThru(opcode)) {
            graph.setNoFallThru(currentBlock);
        }

        if (currentBlock != null && to != null) {
            graph.addEdge(currentBlock, to);
        }
    }

    @Override
    public void visitJumpInsn(final int opcode, final Label label) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, opcode).append(' ');
        final String to = appendLabel(sb, label);
        sb.append('\n');
        addText(sb);
        checkNoFallThru(opcode, to);
    }

    private void addText(final Object t) {
        text.add(t);
        if (currentBlock != null) {
            graph.addText(currentBlock, t.toString());
        }
    }

    @Override
    public void visitLabel(final Label label) {
        final StringBuilder sb = new StringBuilder();
        sb.append("\n");
        final String name = appendLabel(sb, label);
        sb.append(" [bci=");
        sb.append(label.info);
        sb.append("]");
        sb.append("\n");

        graph.addNode(name);
        if (currentBlock != null && !graph.isNoFallThru(currentBlock)) {
            graph.addEdge(currentBlock, name);
        }
        currentBlock = name;
        addText(sb);
    }

    @Override
    public void visitLdcInsn(final Object cst) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, Opcodes.LDC).append(' ');
        if (cst instanceof String) {
            appendStr(sb, (String) cst);
        } else if (cst instanceof Type) {
            sb.append(((Type) cst).getDescriptor()).append(".class");
        } else {
            sb.append(cst);
        }
        sb.append('\n');
        addText(sb);
    }

    @Override
    public void visitIincInsn(final int var, final int increment) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, Opcodes.IINC).append(' ');
        sb.append(var).append(' ')
                .append(increment).append('\n');
        addText(sb);
    }

    @Override
    public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, Opcodes.TABLESWITCH).append(' ');
        for (int i = 0; i < labels.length; ++i) {
            sb.append(tab3).append(min + i).append(": ");
            final String to = appendLabel(sb, labels[i]);
            graph.addEdge(currentBlock, to);
            sb.append('\n');
        }
        sb.append(tab3).append("default: ");
        appendLabel(sb, dflt);
        sb.append('\n');
        addText(sb);
    }

    @Override
    public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, Opcodes.LOOKUPSWITCH).append(' ');
        for (int i = 0; i < labels.length; ++i) {
            sb.append(tab3).append(keys[i]).append(": ");
            final String to = appendLabel(sb, labels[i]);
            graph.addEdge(currentBlock, to);
            sb.append('\n');
        }
        sb.append(tab3).append("default: ");
        final String to = appendLabel(sb, dflt);
        graph.addEdge(currentBlock, to);
        sb.append('\n');
        addText(sb.toString());
    }

    @Override
    public void visitMultiANewArrayInsn(final String desc, final int dims) {
        final StringBuilder sb = new StringBuilder();
        appendOpcode(sb, Opcodes.MULTIANEWARRAY).append(' ');
        appendDescriptor(sb, FIELD_DESCRIPTOR, desc);
        sb.append(' ').append(dims).append('\n');
        addText(sb);
    }

    @Override
    public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type) {
        final StringBuilder sb = new StringBuilder();
        sb.append(tab2).append("try ");
        final String from = appendLabel(sb, start);
        sb.append(' ');
        appendLabel(sb, end);
        sb.append(' ');
        final String to = appendLabel(sb, handler);
        sb.append(' ');
        appendDescriptor(sb, INTERNAL_NAME, type);
        sb.append('\n');
        addText(sb);
        graph.setIsCatch(to, type);
        graph.addTryCatch(from, to);
    }

    @Override
    public void visitLocalVariable(final String name, final String desc,final String signature, final Label start, final Label end, final int index) {

        final StringBuilder sb = new StringBuilder();
        if (!localVarsStarted) {
            text.add("\n");
            localVarsStarted = true;
            graph.addNode("vars");
            currentBlock = "vars";
        }

        sb.append(tab2).append("local ").append(name).append(' ');
        final int len = sb.length();
        for (int i = 0; i < 25 - len; i++) {
            sb.append(' ');
        }
        String label;

        label = appendLabel(sb, start);
        for (int i = 0; i < 5 - label.length(); i++) {
            sb.append(' ');
        }
        label = appendLabel(sb, end);
        for (int i = 0; i < 5 - label.length(); i++) {
            sb.append(' ');
        }

        sb.append(index).append(tab2);

        appendDescriptor(sb, FIELD_DESCRIPTOR, desc);
        sb.append('\n');

        if (signature != null) {
            sb.append(tab2);
            appendDescriptor(sb, FIELD_SIGNATURE, signature);

            final TraceSignatureVisitor sv = new TraceSignatureVisitor(0);
            final SignatureReader r = new SignatureReader(signature);
            r.acceptType(sv);
            sb.append(tab2).append("// declaration: ")
                    .append(sv.getDeclaration()).append('\n');
        }
        addText(sb.toString());
    }

    @Override
    public void visitLineNumber(final int line, final Label start) {
        final StringBuilder sb = new StringBuilder();
        sb.append("<line ");
        sb.append(line);
        sb.append(">\n");
        addText(sb.toString());
    }

    @Override
    public void visitMaxs(final int maxStack, final int maxLocals) {
        final StringBuilder sb = new StringBuilder();
        sb.append('\n');
        sb.append(tab2).append("max stack  = ").append(maxStack);
        sb.append(", max locals = ").append(maxLocals).append('\n');
        addText(sb.toString());
    }

    private void printToDir(final Graph g) {
        if (env._print_code_dir != null) {
            final File dir = new File(env._print_code_dir);
            if (!dir.exists() && !dir.mkdirs()) {
                throw new RuntimeException(dir.toString());
            }

            File file;
            int uniqueId = 0;
            do {
                final String fileName = g.getName() + (uniqueId == 0 ? "" : "_" + uniqueId) +  ".dot";
                file = new File(dir, fileName);
                uniqueId++;
            } while (file.exists());

            try (PrintWriter pw = new PrintWriter(new FileOutputStream(file))) {
                pw.println(g);
            } catch (final FileNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void visitMethodEnd() {
        //here we need to do several bytecode guesses best upon the ldc instructions.
        //for each instruction, assign bci. for an ldc/w/2w, guess a byte and keep
        //iterating. if the next label is wrong, backtrack.
        if (env._print_code_func == null || env._print_code_func.equals(graph.getName())) {
            if (env._print_code_dir != null) {
                printToDir(graph);
            }
        }
    }

    /**
     * Creates a new TraceVisitor instance.
     *
     * @return a new TraceVisitor.
     */
    protected NashornTextifier createNashornTextifier() {
        return new NashornTextifier(env, cr, labelIter, graph);
    }

    private static void appendDescriptor(final StringBuilder sb, final int type, final String desc) {
        if (desc != null) {
            if (type == CLASS_SIGNATURE || type == FIELD_SIGNATURE || type == METHOD_SIGNATURE) {
                sb.append("// signature ").append(desc).append('\n');
            } else {
                appendShortDescriptor(sb, desc);
            }
        }
    }

    private String appendLabel(final StringBuilder sb, final Label l) {
        if (labelNames == null) {
            labelNames = new HashMap<>();
        }
        String name = labelNames.get(l);
        if (name == null) {
            name = "L" + labelNames.size();
            labelNames.put(l, name);
        }
        sb.append(name);
        return name;
    }

    private static void appendHandle(final StringBuilder sb, final Handle h) {
        switch (h.getTag()) {
        case Opcodes.H_GETFIELD:
            sb.append("getfield");
            break;
        case Opcodes.H_GETSTATIC:
            sb.append("getstatic");
            break;
        case Opcodes.H_PUTFIELD:
            sb.append("putfield");
            break;
        case Opcodes.H_PUTSTATIC:
            sb.append("putstatic");
            break;
        case Opcodes.H_INVOKEINTERFACE:
            sb.append("interface");
            break;
        case Opcodes.H_INVOKESPECIAL:
            sb.append("special");
            break;
        case Opcodes.H_INVOKESTATIC:
            sb.append("static");
            break;
        case Opcodes.H_INVOKEVIRTUAL:
            sb.append("virtual");
            break;
        case Opcodes.H_NEWINVOKESPECIAL:
            sb.append("new_special");
            break;
        default:
            assert false;
            break;
        }
        sb.append(" '");
        sb.append(h.getName());
        sb.append("'");
    }

    private static void appendAccess(final StringBuilder sb, final int access) {
        if ((access & Opcodes.ACC_PUBLIC) != 0) {
            sb.append("public ");
        }
        if ((access & Opcodes.ACC_PRIVATE) != 0) {
            sb.append("private ");
        }
        if ((access & Opcodes.ACC_PROTECTED) != 0) {
            sb.append("protected ");
        }
        if ((access & Opcodes.ACC_FINAL) != 0) {
            sb.append("final ");
        }
        if ((access & Opcodes.ACC_STATIC) != 0) {
            sb.append("static ");
        }
        if ((access & Opcodes.ACC_SYNCHRONIZED) != 0) {
            sb.append("synchronized ");
        }
        if ((access & Opcodes.ACC_VOLATILE) != 0) {
            sb.append("volatile ");
        }
        if ((access & Opcodes.ACC_TRANSIENT) != 0) {
            sb.append("transient ");
        }
        if ((access & Opcodes.ACC_ABSTRACT) != 0) {
            sb.append("abstract ");
        }
        if ((access & Opcodes.ACC_STRICT) != 0) {
            sb.append("strictfp ");
        }
        if ((access & Opcodes.ACC_SYNTHETIC) != 0) {
            sb.append("synthetic ");
        }
        if ((access & Opcodes.ACC_MANDATED) != 0) {
            sb.append("mandated ");
        }
        if ((access & Opcodes.ACC_ENUM) != 0) {
            sb.append("enum ");
        }
    }

    private void appendFrameTypes(final StringBuilder sb, final int n, final Object[] o) {
        for (int i = 0; i < n; ++i) {
            if (i > 0) {
                sb.append(' ');
            }
            if (o[i] instanceof String) {
                final String desc = (String) o[i];
                if (desc.startsWith("[")) {
                    appendDescriptor(sb, FIELD_DESCRIPTOR, desc);
                } else {
                    appendDescriptor(sb, INTERNAL_NAME, desc);
                }
            } else if (o[i] instanceof Integer) {
                switch (((Integer)o[i])) {
                case 0:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "T");
                    break;
                case 1:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "I");
                    break;
                case 2:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "F");
                    break;
                case 3:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "D");
                    break;
                case 4:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "J");
                    break;
                case 5:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "N");
                    break;
                case 6:
                    appendDescriptor(sb, FIELD_DESCRIPTOR, "U");
                    break;
                default:
                    assert false;
                    break;
                }
            } else {
                appendLabel(sb, (Label) o[i]);
            }
        }
    }

    private static void appendShortDescriptor(final StringBuilder sb, final String desc) {
        //final StringBuilder buf = new StringBuilder();
        if (desc.charAt(0) == '(') {
            for (int i = 0; i < desc.length(); i++) {
                if (desc.charAt(i) == 'L') {
                    int slash = i;
                    while (desc.charAt(i) != ';') {
                        i++;
                        if (desc.charAt(i) == '/') {
                            slash = i;
                        }
                    }
                    sb.append(desc.substring(slash + 1, i)).append(';');
                } else {
                    sb.append(desc.charAt(i));
                }
            }
        } else {
            final int lastSlash = desc.lastIndexOf('/');
            final int lastBracket = desc.lastIndexOf('[');
            if(lastBracket != -1) {
                sb.append(desc, 0, lastBracket + 1);
            }
            sb.append(lastSlash == -1 ? desc : desc.substring(lastSlash + 1));
        }
    }

    private static void appendStr(final StringBuilder sb, final String s) {
        sb.append('\"');
        for (int i = 0; i < s.length(); ++i) {
            final char c = s.charAt(i);
            if (c == '\n') {
                sb.append("\\n");
            } else if (c == '\r') {
                sb.append("\\r");
            } else if (c == '\\') {
                sb.append("\\\\");
            } else if (c == '"') {
                sb.append("\\\"");
            } else if (c < 0x20 || c > 0x7f) {
                sb.append("\\u");
                if (c < 0x10) {
                    sb.append("000");
                } else if (c < 0x100) {
                    sb.append("00");
                } else if (c < 0x1000) {
                    sb.append('0');
                }
                sb.append(Integer.toString(c, 16));
            } else {
                sb.append(c);
            }
        }
        sb.append('\"');
    }

    private static class Graph {
        private final LinkedHashSet<String> nodes;
        private final Map<String, StringBuilder> contents;
        private final Map<String, Set<String>> edges;
        private final Set<String> hasPreds;
        private final Set<String> noFallThru;
        private final Map<String, String> catches;
        private final Map<String, Set<String>> exceptionMap; //maps catch nodes to all their trys that can reach them
        private final String name;

        private static final String LEFT_ALIGN      = "\\l";
        private static final String COLOR_CATCH     = "\"#ee9999\"";
        private static final String COLOR_ORPHAN    = "\"#9999bb\"";
        private static final String COLOR_DEFAULT   = "\"#99bb99\"";
        private static final String COLOR_LOCALVARS = "\"#999999\"";

        Graph(final String name) {
            this.name         = name;
            this.nodes        = new LinkedHashSet<>();
            this.contents     = new HashMap<>();
            this.edges        = new HashMap<>();
            this.hasPreds     = new HashSet<>();
            this.catches      = new HashMap<>();
            this.noFallThru   = new HashSet<>();
            this.exceptionMap = new HashMap<>();
         }

        void addEdge(final String from, final String to) {
            Set<String> edgeSet = edges.get(from);
            if (edgeSet == null) {
                edgeSet = new LinkedHashSet<>();
                edges.put(from, edgeSet);
            }
            edgeSet.add(to);
            hasPreds.add(to);
        }

        void addTryCatch(final String tryNode, final String catchNode) {
            Set<String> tryNodes = exceptionMap.get(catchNode);
            if (tryNodes == null) {
                tryNodes = new HashSet<>();
                exceptionMap.put(catchNode, tryNodes);
            }
            if (!tryNodes.contains(tryNode)) {
                addEdge(tryNode, catchNode);
            }
            tryNodes.add(tryNode);
        }

        void addNode(final String node) {
            assert !nodes.contains(node);
            nodes.add(node);
        }

        void setNoFallThru(final String node) {
            noFallThru.add(node);
        }

        boolean isNoFallThru(final String node) {
            return noFallThru.contains(node);
        }

        void setIsCatch(final String node, final String exception) {
            catches.put(node, exception);
        }

        String getName() {
            return name;
        }

        void addText(final String node, final String text) {
            StringBuilder sb = contents.get(node);
            if (sb == null) {
                sb = new StringBuilder();
            }

            for (int i = 0; i < text.length(); i++) {
                switch (text.charAt(i)) {
                case '\n':
                    sb.append(LEFT_ALIGN);
                    break;
                case '"':
                    sb.append("'");
                    break;
                default:
                    sb.append(text.charAt(i));
                    break;
                }
           }

            contents.put(node, sb);
        }

        private static String dottyFriendly(final String name) {
            return name.replace(':', '_');
        }

        @Override
        public String toString() {

            final StringBuilder sb = new StringBuilder();
            sb.append("digraph ").append(dottyFriendly(name)).append(" {");
            sb.append("\n");
            sb.append("\tgraph [fontname=courier]\n");
            sb.append("\tnode [style=filled,color="+COLOR_DEFAULT+",fontname=courier]\n");
            sb.append("\tedge [fontname=courier]\n\n");

            for (final String node : nodes) {
                sb.append("\t");
                sb.append(node);
                sb.append(" [");
                sb.append("id=");
                sb.append(node);
                sb.append(", label=\"");
                String c = contents.get(node).toString();
                if (c.startsWith(LEFT_ALIGN)) {
                    c = c.substring(LEFT_ALIGN.length());
                }
                final String ex = catches.get(node);
                if (ex != null) {
                    sb.append("*** CATCH: ").append(ex).append(" ***\\l");
                }
                sb.append(c);
                sb.append("\"]\n");
            }

            for (final String from : edges.keySet()) {
                for (final String to : edges.get(from)) {
                    sb.append("\t");
                    sb.append(from);
                    sb.append(" -> ");
                    sb.append(to);
                    sb.append("[label=\"");
                    sb.append(to);
                    sb.append("\"");
                    if (catches.get(to) != null) {
                        sb.append(", color=red, style=dashed");
                    }
                    sb.append(']');
                    sb.append(";\n");
                }
            }

            sb.append("\n");
            for (final String node : nodes) {
                sb.append("\t");
                sb.append(node);
                sb.append(" [shape=box");
                if (catches.get(node) != null) {
                    sb.append(", color=" + COLOR_CATCH);
                } else if ("vars".equals(node)) {
                    sb.append(", shape=hexagon, color=" + COLOR_LOCALVARS);
                } else if (!hasPreds.contains(node)) {
                    sb.append(", color=" + COLOR_ORPHAN);
                }
                sb.append("]\n");
            }

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

    static class NashornLabel extends Label {
        final Label label;
        final int   bci;
        final int   opcode;

        NashornLabel(final Label label, final int bci) {
            this.label = label;
            this.bci   = bci;
            this.opcode = -1;
        }

        //not an ASM label
        NashornLabel(final int opcode, final int bci) {
            this.opcode = opcode;
            this.bci = bci;
            this.label = null;
        }

        Label getLabel() {
            return label;
        }

        @Override
        public int getOffset() {
            return bci;
        }

        @Override
        public String toString() {
            return "label " + bci;
        }
    }

    @Override
    public Printer visitAnnotationDefault() {
        throw new AssertionError();
    }

    @Override
    public Printer visitClassAnnotation(final String arg0, final boolean arg1) {
        return this;
    }

    @Override
    public void visitClassAttribute(final Attribute arg0) {
        throw new AssertionError();
    }

    @Override
    public Printer visitFieldAnnotation(final String arg0, final boolean arg1) {
        throw new AssertionError();
    }

    @Override
    public void visitFieldAttribute(final Attribute arg0) {
        throw new AssertionError();
    }

    @Override
    public Printer visitMethodAnnotation(final String arg0, final boolean arg1) {
        return this;
    }

    @Override
    public void visitMethodAttribute(final Attribute arg0) {
        throw new AssertionError();
    }

    @Override
    public Printer visitParameterAnnotation(final int arg0, final String arg1, final boolean arg2) {
        throw new AssertionError();
    }

    @Override
    public void visit(final String arg0, final Object arg1) {
        throw new AssertionError();
    }

    @Override
    public Printer visitAnnotation(final String arg0, final String arg1) {
        throw new AssertionError();
    }

    @Override
    public void visitAnnotationEnd() {
        //empty
    }

    @Override
    public Printer visitArray(final String arg0) {
        throw new AssertionError();
    }

    @Override
    public void visitEnum(final String arg0, final String arg1, final String arg2) {
        throw new AssertionError();
    }

    @Override
    public void visitInnerClass(final String arg0, final String arg1, final String arg2, final int arg3) {
        throw new AssertionError();
    }
}
