/**
 * ASM: a very small and fast Java bytecode manipulation framework
 * Copyright (c) 2000-2011 INRIA, France Telecom
 * All rights reserved.
 * <p/>
 * Redistribution and use in srccode and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of srccode code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 3. Neither the className of the copyright holders nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * <p/>
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.beetl.ow2.asm.util;

import org.beetl.ow2.asm.signature.SignatureReader;
import org.beetl.ow2.asm.*;

import java.io.FileInputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * A {@link Printer} that prints a disassembled view of the classes it visits.
 *
 * @author Eric Bruneton
 */
public class Textifier extends Printer {

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for internal
     * type names in bytecode notation.
     */
    public static final int INTERNAL_NAME = 0;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for field
     * descriptors, formatted in bytecode notation
     */
    public static final int FIELD_DESCRIPTOR = 1;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for field
     * signatures, formatted in bytecode notation
     */
    public static final int FIELD_SIGNATURE = 2;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for method
     * descriptors, formatted in bytecode notation
     */
    public static final int METHOD_DESCRIPTOR = 3;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for method
     * signatures, formatted in bytecode notation
     */
    public static final int METHOD_SIGNATURE = 4;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for class
     * signatures, formatted in bytecode notation
     */
    public static final int CLASS_SIGNATURE = 5;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for field or
     * method return value signatures, formatted in default Java notation
     * (non-bytecode)
     */
    public static final int TYPE_DECLARATION = 6;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for class
     * signatures, formatted in default Java notation (non-bytecode)
     */
    public static final int CLASS_DECLARATION = 7;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for method
     * parameter signatures, formatted in default Java notation (non-bytecode)
     */
    public static final int PARAMETERS_DECLARATION = 8;

    /**
     * Constant used in {@link #appendDescriptor appendDescriptor} for handle
     * descriptors, formatted in bytecode notation
     */
    public static final int HANDLE_DESCRIPTOR = 9;

    /**
     * Tab for class members.
     */
    protected String tab = "  ";

    /**
     * Tab for bytecode instructions.
     */
    protected String tab2 = "    ";

    /**
     * Tab for table and lookup switch instructions.
     */
    protected String tab3 = "      ";

    /**
     * Tab for labels.
     */
    protected String ltab = "   ";

    /**
     * The label names. This map associate String values to Label keys.
     */
    protected Map<Label, String> labelNames;

    /**
     * Class access flags
     */
    private int access;

    private int valueNumber = 0;

    /**
     * Constructs a new {@link Textifier}. <i>Subclasses must not use this
     * constructor</i>. Instead, they must use the {@link #Textifier(int)}
     * version.
     *
     * @throws IllegalStateException
     *             If a subclass calls this constructor.
     */
    public Textifier() {
        this(Opcodes.ASM5);
        if (getClass() != Textifier.class) {
            throw new IllegalStateException();
        }
    }

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

    /**
     * Prints a disassembled view of the given class to the standard output.
     * <p>
     * Usage: Textifier [-debug] &lt;binary class className or class file className &gt;
     *
     * @param args
     *            the command line arguments.
     *
     * @throws Exception
     *             if the class cannot be found, or if an IO exception occurs.
     */
    public static void main(final String[] args) throws Exception {
        int i = 0;
        int flags = ClassReader.SKIP_DEBUG;

        boolean ok = true;
        if (args.length < 1 || args.length > 2) {
            ok = false;
        }
        if (ok && "-debug".equals(args[0])) {
            i = 1;
            flags = 0;
            if (args.length != 2) {
                ok = false;
            }
        }
        if (!ok) {
            System.err
                    .println("Prints a disassembled view of the given class.");
            System.err.println("Usage: Textifier [-debug] "
                    + "<fully qualified class name or class file name>");
            return;
        }
        ClassReader cr;
        if (args[i].endsWith(".class") || args[i].indexOf('\\') > -1
                || args[i].indexOf('/') > -1) {
            cr = new ClassReader(new FileInputStream(args[i]));
        } else {
            cr = new ClassReader(args[i]);
        }
        cr.accept(new TraceClassVisitor(new PrintWriter(System.out)), flags);
    }

    // ------------------------------------------------------------------------
    // Classes
    // ------------------------------------------------------------------------

    @Override
    public void visit(final int version, final int access, final String name,
                      final String signature, final String superName,
                      final String[] interfaces) {
        this.access = access;
        int major = version & 0xFFFF;
        int minor = version >>> 16;
        buf.setLength(0);
        buf.append("// class version ").append(major).append('.').append(minor)
                .append(" (").append(version).append(")\n");
        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            buf.append("// DEPRECATED\n");
        }
        buf.append("// access flags 0x")
                .append(Integer.toHexString(access).toUpperCase()).append('\n');

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

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

        if (superName != null && !"java/lang/Object".equals(superName)) {
            buf.append(" extends ");
            appendDescriptor(INTERNAL_NAME, superName);
            buf.append(' ');
        }
        if (interfaces != null && interfaces.length > 0) {
            buf.append(" implements ");
            for (int i = 0; i < interfaces.length; ++i) {
                appendDescriptor(INTERNAL_NAME, interfaces[i]);
                buf.append(' ');
            }
        }
        buf.append(" {\n\n");

        text.add(buf.toString());
    }

    @Override
    public void visitSource(final String file, final String debug) {
        buf.setLength(0);
        if (file != null) {
            buf.append(tab).append("// compiled from: ").append(file)
                    .append('\n');
        }
        if (debug != null) {
            buf.append(tab).append("// debug info: ").append(debug)
                    .append('\n');
        }
        if (buf.length() > 0) {
            text.add(buf.toString());
        }
    }

    @Override
    public void visitOuterClass(final String owner, final String name,
                                final String desc) {
        buf.setLength(0);
        buf.append(tab).append("OUTERCLASS ");
        appendDescriptor(INTERNAL_NAME, owner);
        buf.append(' ');
        if (name != null) {
            buf.append(name).append(' ');
        }
        appendDescriptor(METHOD_DESCRIPTOR, desc);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public Textifier visitClassAnnotation(final String desc,
                                          final boolean visible) {
        text.add("\n");
        return visitAnnotation(desc, visible);
    }

    @Override
    public Printer visitClassTypeAnnotation(int typeRef, TypePath typePath,
                                            String desc, boolean visible) {
        text.add("\n");
        return visitTypeAnnotation(typeRef, typePath, desc, visible);
    }

    @Override
    public void visitClassAttribute(final Attribute attr) {
        text.add("\n");
        visitAttribute(attr);
    }

    @Override
    public void visitInnerClass(final String name, final String outerName,
                                final String innerName, final int access) {
        buf.setLength(0);
        buf.append(tab).append("// access flags 0x");
        buf.append(
                Integer.toHexString(access & ~Opcodes.ACC_SUPER).toUpperCase())
                .append('\n');
        buf.append(tab);
        appendAccess(access);
        buf.append("INNERCLASS ");
        appendDescriptor(INTERNAL_NAME, name);
        buf.append(' ');
        appendDescriptor(INTERNAL_NAME, outerName);
        buf.append(' ');
        appendDescriptor(INTERNAL_NAME, innerName);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public Textifier visitField(final int access, final String name,
                                final String desc, final String signature, final Object value) {
        buf.setLength(0);
        buf.append('\n');
        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            buf.append(tab).append("// DEPRECATED\n");
        }
        buf.append(tab).append("// access flags 0x")
                .append(Integer.toHexString(access).toUpperCase()).append('\n');
        if (signature != null) {
            buf.append(tab);
            appendDescriptor(FIELD_SIGNATURE, signature);

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

        buf.append(tab);
        appendAccess(access);

        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append(' ').append(name);
        if (value != null) {
            buf.append(" = ");
            if (value instanceof String) {
                buf.append('\"').append(value).append('\"');
            } else {
                buf.append(value);
            }
        }

        buf.append('\n');
        text.add(buf.toString());

        Textifier t = createTextifier();
        text.add(t.getText());
        return t;
    }

    @Override
    public Textifier visitMethod(final int access, final String name,
                                 final String desc, final String signature, final String[] exceptions) {
        buf.setLength(0);
        buf.append('\n');
        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            buf.append(tab).append("// DEPRECATED\n");
        }
        buf.append(tab).append("// access flags 0x")
                .append(Integer.toHexString(access).toUpperCase()).append('\n');

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

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

            buf.append(tab).append("// declaration: ").append(genericReturn)
                    .append(' ').append(name).append(genericDecl);
            if (genericExceptions != null) {
                buf.append(" throws ").append(genericExceptions);
            }
            buf.append('\n');
        }

        buf.append(tab);
        appendAccess(access & ~Opcodes.ACC_VOLATILE);
        if ((access & Opcodes.ACC_NATIVE) != 0) {
            buf.append("native ");
        }
        if ((access & Opcodes.ACC_VARARGS) != 0) {
            buf.append("varargs ");
        }
        if ((access & Opcodes.ACC_BRIDGE) != 0) {
            buf.append("bridge ");
        }
        if ((this.access & Opcodes.ACC_INTERFACE) != 0
                && (access & Opcodes.ACC_ABSTRACT) == 0
                && (access & Opcodes.ACC_STATIC) == 0) {
            buf.append("default ");
        }

        buf.append(name);
        appendDescriptor(METHOD_DESCRIPTOR, desc);
        if (exceptions != null && exceptions.length > 0) {
            buf.append(" throws ");
            for (int i = 0; i < exceptions.length; ++i) {
                appendDescriptor(INTERNAL_NAME, exceptions[i]);
                buf.append(' ');
            }
        }

        buf.append('\n');
        text.add(buf.toString());

        Textifier t = createTextifier();
        text.add(t.getText());
        return t;
    }

    @Override
    public void visitClassEnd() {
        text.add("}\n");
    }

    // ------------------------------------------------------------------------
    // Annotations
    // ------------------------------------------------------------------------

    @Override
    public void visit(final String name, final Object value) {
        buf.setLength(0);
        appendComa(valueNumber++);

        if (name != null) {
            buf.append(name).append('=');
        }

        if (value instanceof String) {
            visitString((String) value);
        } else if (value instanceof Type) {
            visitType((Type) value);
        } else if (value instanceof Byte) {
            visitByte(((Byte) value).byteValue());
        } else if (value instanceof Boolean) {
            visitBoolean(((Boolean) value).booleanValue());
        } else if (value instanceof Short) {
            visitShort(((Short) value).shortValue());
        } else if (value instanceof Character) {
            visitChar(((Character) value).charValue());
        } else if (value instanceof Integer) {
            visitInt(((Integer) value).intValue());
        } else if (value instanceof Float) {
            visitFloat(((Float) value).floatValue());
        } else if (value instanceof Long) {
            visitLong(((Long) value).longValue());
        } else if (value instanceof Double) {
            visitDouble(((Double) value).doubleValue());
        } else if (value.getClass().isArray()) {
            buf.append('{');
            if (value instanceof byte[]) {
                byte[] v = (byte[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitByte(v[i]);
                }
            } else if (value instanceof boolean[]) {
                boolean[] v = (boolean[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitBoolean(v[i]);
                }
            } else if (value instanceof short[]) {
                short[] v = (short[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitShort(v[i]);
                }
            } else if (value instanceof char[]) {
                char[] v = (char[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitChar(v[i]);
                }
            } else if (value instanceof int[]) {
                int[] v = (int[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitInt(v[i]);
                }
            } else if (value instanceof long[]) {
                long[] v = (long[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitLong(v[i]);
                }
            } else if (value instanceof float[]) {
                float[] v = (float[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitFloat(v[i]);
                }
            } else if (value instanceof double[]) {
                double[] v = (double[]) value;
                for (int i = 0; i < v.length; i++) {
                    appendComa(i);
                    visitDouble(v[i]);
                }
            }
            buf.append('}');
        }

        text.add(buf.toString());
    }

    private void visitInt(final int value) {
        buf.append(value);
    }

    private void visitLong(final long value) {
        buf.append(value).append('L');
    }

    private void visitFloat(final float value) {
        buf.append(value).append('F');
    }

    private void visitDouble(final double value) {
        buf.append(value).append('D');
    }

    private void visitChar(final char value) {
        buf.append("(char)").append((int) value);
    }

    private void visitShort(final short value) {
        buf.append("(short)").append(value);
    }

    private void visitByte(final byte value) {
        buf.append("(byte)").append(value);
    }

    private void visitBoolean(final boolean value) {
        buf.append(value);
    }

    private void visitString(final String value) {
        appendString(buf, value);
    }

    private void visitType(final Type value) {
        buf.append(value.getClassName()).append(".class");
    }

    @Override
    public void visitEnum(final String name, final String desc,
                          final String value) {
        buf.setLength(0);
        appendComa(valueNumber++);
        if (name != null) {
            buf.append(name).append('=');
        }
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('.').append(value);
        text.add(buf.toString());
    }

    @Override
    public Textifier visitAnnotation(final String name, final String desc) {
        buf.setLength(0);
        appendComa(valueNumber++);
        if (name != null) {
            buf.append(name).append('=');
        }
        buf.append('@');
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('(');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        text.add(")");
        return t;
    }

    @Override
    public Textifier visitArray(final String name) {
        buf.setLength(0);
        appendComa(valueNumber++);
        if (name != null) {
            buf.append(name).append('=');
        }
        buf.append('{');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        text.add("}");
        return t;
    }

    @Override
    public void visitAnnotationEnd() {
    }

    // ------------------------------------------------------------------------
    // Fields
    // ------------------------------------------------------------------------

    @Override
    public Textifier visitFieldAnnotation(final String desc,
                                          final boolean visible) {
        return visitAnnotation(desc, visible);
    }

    @Override
    public Printer visitFieldTypeAnnotation(int typeRef, TypePath typePath,
                                            String desc, boolean visible) {
        return visitTypeAnnotation(typeRef, typePath, desc, visible);
    }

    @Override
    public void visitFieldAttribute(final Attribute attr) {
        visitAttribute(attr);
    }

    @Override
    public void visitFieldEnd() {
    }

    // ------------------------------------------------------------------------
    // Methods
    // ------------------------------------------------------------------------

    @Override
    public void visitParameter(final String name, final int access) {
        buf.setLength(0);
        buf.append(tab2).append("// parameter ");
        appendAccess(access);
        buf.append(' ').append((name == null) ? "<no name>" : name)
                .append('\n');
        text.add(buf.toString());
    }

    @Override
    public Textifier visitAnnotationDefault() {
        text.add(tab2 + "default=");
        Textifier t = createTextifier();
        text.add(t.getText());
        text.add("\n");
        return t;
    }

    @Override
    public Textifier visitMethodAnnotation(final String desc,
                                           final boolean visible) {
        return visitAnnotation(desc, visible);
    }

    @Override
    public Printer visitMethodTypeAnnotation(int typeRef, TypePath typePath,
                                             String desc, boolean visible) {
        return visitTypeAnnotation(typeRef, typePath, desc, visible);
    }

    @Override
    public Textifier visitParameterAnnotation(final int parameter,
                                              final String desc, final boolean visible) {
        buf.setLength(0);
        buf.append(tab2).append('@');
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('(');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        text.add(visible ? ") // parameter " : ") // invisible, parameter ");
        text.add(new Integer(parameter));
        text.add("\n");
        return t;
    }

    @Override
    public void visitMethodAttribute(final Attribute attr) {
        buf.setLength(0);
        buf.append(tab).append("ATTRIBUTE ");
        appendDescriptor(-1, attr.type);

        if (attr instanceof Textifiable) {
            ((Textifiable) attr).textify(buf, labelNames);
        } else {
            buf.append(" : unknown\n");
        }

        text.add(buf.toString());
    }

    @Override
    public void visitCode() {
    }

    @Override
    public void visitFrame(final int type, final int nLocal,
                           final Object[] local, final int nStack, final Object[] stack) {
        buf.setLength(0);
        buf.append(ltab);
        buf.append("FRAME ");
        switch (type) {
            case Opcodes.F_NEW:
            case Opcodes.F_FULL:
                buf.append("FULL [");
                appendFrameTypes(nLocal, local);
                buf.append("] [");
                appendFrameTypes(nStack, stack);
                buf.append(']');
                break;
            case Opcodes.F_APPEND:
                buf.append("APPEND [");
                appendFrameTypes(nLocal, local);
                buf.append(']');
                break;
            case Opcodes.F_CHOP:
                buf.append("CHOP ").append(nLocal);
                break;
            case Opcodes.F_SAME:
                buf.append("SAME");
                break;
            case Opcodes.F_SAME1:
                buf.append("SAME1 ");
                appendFrameTypes(1, stack);
                break;
        }
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitInsn(final int opcode) {
        buf.setLength(0);
        buf.append(tab2).append(OPCODES[opcode]).append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitIntInsn(final int opcode, final int operand) {
        buf.setLength(0);
        buf.append(tab2)
                .append(OPCODES[opcode])
                .append(' ')
                .append(opcode == Opcodes.NEWARRAY ? TYPES[operand] : Integer
                        .toString(operand)).append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitVarInsn(final int opcode, final int var) {
        buf.setLength(0);
        buf.append(tab2).append(OPCODES[opcode]).append(' ').append(var)
                .append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitTypeInsn(final int opcode, final String type) {
        buf.setLength(0);
        buf.append(tab2).append(OPCODES[opcode]).append(' ');
        appendDescriptor(INTERNAL_NAME, type);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitFieldInsn(final int opcode, final String owner,
                               final String name, final String desc) {
        buf.setLength(0);
        buf.append(tab2).append(OPCODES[opcode]).append(' ');
        appendDescriptor(INTERNAL_NAME, owner);
        buf.append('.').append(name).append(" : ");
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Deprecated
    @Override
    public void visitMethodInsn(final int opcode, final String owner,
                                final String name, final String desc) {
        if (api >= Opcodes.ASM5) {
            super.visitMethodInsn(opcode, owner, name, desc);
            return;
        }
        doVisitMethodInsn(opcode, owner, name, desc,
                opcode == Opcodes.INVOKEINTERFACE);
    }

    @Override
    public void visitMethodInsn(final int opcode, final String owner,
                                final String name, final String desc, final boolean itf) {
        if (api < Opcodes.ASM5) {
            super.visitMethodInsn(opcode, owner, name, desc, itf);
            return;
        }
        doVisitMethodInsn(opcode, owner, name, desc, itf);
    }

    private void doVisitMethodInsn(final int opcode, final String owner,
                                   final String name, final String desc, final boolean itf) {
        buf.setLength(0);
        buf.append(tab2).append(OPCODES[opcode]).append(' ');
        appendDescriptor(INTERNAL_NAME, owner);
        buf.append('.').append(name).append(' ');
        appendDescriptor(METHOD_DESCRIPTOR, desc);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm,
                                       Object... bsmArgs) {
        buf.setLength(0);
        buf.append(tab2).append("INVOKEDYNAMIC").append(' ');
        buf.append(name);
        appendDescriptor(METHOD_DESCRIPTOR, desc);
        buf.append(" [");
        buf.append('\n');
        buf.append(tab3);
        appendHandle(bsm);
        buf.append('\n');
        buf.append(tab3).append("// arguments:");
        if (bsmArgs.length == 0) {
            buf.append(" none");
        } else {
            buf.append('\n');
            for (int i = 0; i < bsmArgs.length; i++) {
                buf.append(tab3);
                Object cst = bsmArgs[i];
                if (cst instanceof String) {
                    appendString(buf, (String) cst);
                } else if (cst instanceof Type) {
                    Type type = (Type) cst;
                    if (type.getSort() == Type.METHOD) {
                        appendDescriptor(METHOD_DESCRIPTOR, type.getDescriptor());
                    } else {
                        buf.append(type.getDescriptor()).append(".class");
                    }
                } else if (cst instanceof Handle) {
                    appendHandle((Handle) cst);
                } else {
                    buf.append(cst);
                }
                buf.append(", \n");
            }
            buf.setLength(buf.length() - 3);
        }
        buf.append('\n');
        buf.append(tab2).append("]\n");
        text.add(buf.toString());
    }

    @Override
    public void visitJumpInsn(final int opcode, final Label label) {
        buf.setLength(0);
        buf.append(tab2).append(OPCODES[opcode]).append(' ');
        appendLabel(label);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitLabel(final Label label) {
        buf.setLength(0);
        buf.append(ltab);
        appendLabel(label);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitLdcInsn(final Object cst) {
        buf.setLength(0);
        buf.append(tab2).append("LDC ");
        if (cst instanceof String) {
            appendString(buf, (String) cst);
        } else if (cst instanceof Type) {
            buf.append(((Type) cst).getDescriptor()).append(".class");
        } else {
            buf.append(cst);
        }
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitIincInsn(final int var, final int increment) {
        buf.setLength(0);
        buf.append(tab2).append("IINC ").append(var).append(' ')
                .append(increment).append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitTableSwitchInsn(final int min, final int max,
                                     final Label dflt, final Label... labels) {
        buf.setLength(0);
        buf.append(tab2).append("TABLESWITCH\n");
        for (int i = 0; i < labels.length; ++i) {
            buf.append(tab3).append(min + i).append(": ");
            appendLabel(labels[i]);
            buf.append('\n');
        }
        buf.append(tab3).append("default: ");
        appendLabel(dflt);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitLookupSwitchInsn(final Label dflt, final int[] keys,
                                      final Label[] labels) {
        buf.setLength(0);
        buf.append(tab2).append("LOOKUPSWITCH\n");
        for (int i = 0; i < labels.length; ++i) {
            buf.append(tab3).append(keys[i]).append(": ");
            appendLabel(labels[i]);
            buf.append('\n');
        }
        buf.append(tab3).append("default: ");
        appendLabel(dflt);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitMultiANewArrayInsn(final String desc, final int dims) {
        buf.setLength(0);
        buf.append(tab2).append("MULTIANEWARRAY ");
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append(' ').append(dims).append('\n');
        text.add(buf.toString());
    }

    @Override
    public Printer visitInsnAnnotation(int typeRef, TypePath typePath,
                                       String desc, boolean visible) {
        return visitTypeAnnotation(typeRef, typePath, desc, visible);
    }

    @Override
    public void visitTryCatchBlock(final Label start, final Label end,
                                   final Label handler, final String type) {
        buf.setLength(0);
        buf.append(tab2).append("TRYCATCHBLOCK ");
        appendLabel(start);
        buf.append(' ');
        appendLabel(end);
        buf.append(' ');
        appendLabel(handler);
        buf.append(' ');
        appendDescriptor(INTERNAL_NAME, type);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public Printer visitTryCatchAnnotation(int typeRef, TypePath typePath,
                                           String desc, boolean visible) {
        buf.setLength(0);
        buf.append(tab2).append("TRYCATCHBLOCK @");
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('(');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        buf.setLength(0);
        buf.append(") : ");
        appendTypeReference(typeRef);
        buf.append(", ").append(typePath);
        buf.append(visible ? "\n" : " // invisible\n");
        text.add(buf.toString());
        return t;
    }

    @Override
    public void visitLocalVariable(final String name, final String desc,
                                   final String signature, final Label start, final Label end,
                                   final int index) {
        buf.setLength(0);
        buf.append(tab2).append("LOCALVARIABLE ").append(name).append(' ');
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append(' ');
        appendLabel(start);
        buf.append(' ');
        appendLabel(end);
        buf.append(' ').append(index).append('\n');

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

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

    @Override
    public Printer visitLocalVariableAnnotation(int typeRef, TypePath typePath,
                                                Label[] start, Label[] end, int[] index, String desc,
                                                boolean visible) {
        buf.setLength(0);
        buf.append(tab2).append("LOCALVARIABLE @");
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('(');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        buf.setLength(0);
        buf.append(") : ");
        appendTypeReference(typeRef);
        buf.append(", ").append(typePath);
        for (int i = 0; i < start.length; ++i) {
            buf.append(" [ ");
            appendLabel(start[i]);
            buf.append(" - ");
            appendLabel(end[i]);
            buf.append(" - ").append(index[i]).append(" ]");
        }
        buf.append(visible ? "\n" : " // invisible\n");
        text.add(buf.toString());
        return t;
    }

    @Override
    public void visitLineNumber(final int line, final Label start) {
        buf.setLength(0);
        buf.append(tab2).append("LINENUMBER ").append(line).append(' ');
        appendLabel(start);
        buf.append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitMaxs(final int maxStack, final int maxLocals) {
        buf.setLength(0);
        buf.append(tab2).append("MAXSTACK = ").append(maxStack).append('\n');
        text.add(buf.toString());

        buf.setLength(0);
        buf.append(tab2).append("MAXLOCALS = ").append(maxLocals).append('\n');
        text.add(buf.toString());
    }

    @Override
    public void visitMethodEnd() {
    }

    // ------------------------------------------------------------------------
    // Common methods
    // ------------------------------------------------------------------------

    /**
     * Prints a disassembled view of the given annotation.
     *
     * @param desc
     *            the class descriptor of the annotation class.
     * @param visible
     *            <tt>true</tt> if the annotation is visible at runtime.
     * @return a visitor to visit the annotation values.
     */
    public Textifier visitAnnotation(final String desc, final boolean visible) {
        buf.setLength(0);
        buf.append(tab).append('@');
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('(');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        text.add(visible ? ")\n" : ") // invisible\n");
        return t;
    }

    /**
     * Prints a disassembled view of the given type annotation.
     *
     * @param typeRef
     *            a reference to the annotated type. See {@link TypeReference}.
     * @param typePath
     *            the path to the annotated type argument, wildcard bound, array
     *            element type, or static inner type within 'typeRef'. May be
     *            <tt>null</tt> if the annotation targets 'typeRef' as a whole.
     * @param desc
     *            the class descriptor of the annotation class.
     * @param visible
     *            <tt>true</tt> if the annotation is visible at runtime.
     * @return a visitor to visit the annotation values.
     */
    public Textifier visitTypeAnnotation(final int typeRef,
                                         final TypePath typePath, final String desc, final boolean visible) {
        buf.setLength(0);
        buf.append(tab).append('@');
        appendDescriptor(FIELD_DESCRIPTOR, desc);
        buf.append('(');
        text.add(buf.toString());
        Textifier t = createTextifier();
        text.add(t.getText());
        buf.setLength(0);
        buf.append(") : ");
        appendTypeReference(typeRef);
        buf.append(", ").append(typePath);
        buf.append(visible ? "\n" : " // invisible\n");
        text.add(buf.toString());
        return t;
    }

    /**
     * Prints a disassembled view of the given attribute.
     *
     * @param attr
     *            an attribute.
     */
    public void visitAttribute(final Attribute attr) {
        buf.setLength(0);
        buf.append(tab).append("ATTRIBUTE ");
        appendDescriptor(-1, attr.type);

        if (attr instanceof Textifiable) {
            ((Textifiable) attr).textify(buf, null);
        } else {
            buf.append(" : unknown\n");
        }

        text.add(buf.toString());
    }

    // ------------------------------------------------------------------------
    // Utility methods
    // ------------------------------------------------------------------------

    /**
     * Creates a new TraceVisitor instance.
     *
     * @return a new TraceVisitor.
     */
    protected Textifier createTextifier() {
        return new Textifier();
    }

    /**
     * Appends an internal className, a type descriptor or a type signature to
     * {@link #buf buf}.
     *
     * @param type
     *            indicates if desc is an internal className, a field descriptor, a
     *            method descriptor, a class signature, ...
     * @param desc
     *            an internal className, type descriptor, or type signature. May be
     *            <tt>null</tt>.
     */
    protected void appendDescriptor(final int type, final String desc) {
        if (type == CLASS_SIGNATURE || type == FIELD_SIGNATURE
                || type == METHOD_SIGNATURE) {
            if (desc != null) {
                buf.append("// signature ").append(desc).append('\n');
            }
        } else {
            buf.append(desc);
        }
    }

    /**
     * Appends the className of the given label to {@link #buf buf}. Creates a new
     * label className if the given label does not yet have one.
     *
     * @param l
     *            a label.
     */
    protected void appendLabel(final Label l) {
        if (labelNames == null) {
            labelNames = new HashMap<Label, String>();
        }
        String name = labelNames.get(l);
        if (name == null) {
            name = "L" + labelNames.size();
            labelNames.put(l, name);
        }
        buf.append(name);
    }

    /**
     * Appends the information about the given handle to {@link #buf buf}.
     *
     * @param h
     *            a handle, non null.
     */
    protected void appendHandle(final Handle h) {
        int tag = h.getTag();
        buf.append("// handle kind 0x").append(Integer.toHexString(tag))
                .append(" : ");
        boolean isMethodHandle = false;
        switch (tag) {
            case Opcodes.H_GETFIELD:
                buf.append("GETFIELD");
                break;
            case Opcodes.H_GETSTATIC:
                buf.append("GETSTATIC");
                break;
            case Opcodes.H_PUTFIELD:
                buf.append("PUTFIELD");
                break;
            case Opcodes.H_PUTSTATIC:
                buf.append("PUTSTATIC");
                break;
            case Opcodes.H_INVOKEINTERFACE:
                buf.append("INVOKEINTERFACE");
                isMethodHandle = true;
                break;
            case Opcodes.H_INVOKESPECIAL:
                buf.append("INVOKESPECIAL");
                isMethodHandle = true;
                break;
            case Opcodes.H_INVOKESTATIC:
                buf.append("INVOKESTATIC");
                isMethodHandle = true;
                break;
            case Opcodes.H_INVOKEVIRTUAL:
                buf.append("INVOKEVIRTUAL");
                isMethodHandle = true;
                break;
            case Opcodes.H_NEWINVOKESPECIAL:
                buf.append("NEWINVOKESPECIAL");
                isMethodHandle = true;
                break;
        }
        buf.append('\n');
        buf.append(tab3);
        appendDescriptor(INTERNAL_NAME, h.getOwner());
        buf.append('.');
        buf.append(h.getName());
        if (!isMethodHandle) {
            buf.append('(');
        }
        appendDescriptor(HANDLE_DESCRIPTOR, h.getDesc());
        if (!isMethodHandle) {
            buf.append(')');
        }
    }

    /**
     * Appends a string representation of the given access modifiers to
     * {@link #buf buf}.
     *
     * @param access
     *            some access modifiers.
     */
    private void appendAccess(final int access) {
        if ((access & Opcodes.ACC_PUBLIC) != 0) {
            buf.append("public ");
        }
        if ((access & Opcodes.ACC_PRIVATE) != 0) {
            buf.append("private ");
        }
        if ((access & Opcodes.ACC_PROTECTED) != 0) {
            buf.append("protected ");
        }
        if ((access & Opcodes.ACC_FINAL) != 0) {
            buf.append("final ");
        }
        if ((access & Opcodes.ACC_STATIC) != 0) {
            buf.append("static ");
        }
        if ((access & Opcodes.ACC_SYNCHRONIZED) != 0) {
            buf.append("synchronized ");
        }
        if ((access & Opcodes.ACC_VOLATILE) != 0) {
            buf.append("volatile ");
        }
        if ((access & Opcodes.ACC_TRANSIENT) != 0) {
            buf.append("transient ");
        }
        if ((access & Opcodes.ACC_ABSTRACT) != 0) {
            buf.append("abstract ");
        }
        if ((access & Opcodes.ACC_STRICT) != 0) {
            buf.append("strictfp ");
        }
        if ((access & Opcodes.ACC_SYNTHETIC) != 0) {
            buf.append("synthetic ");
        }
        if ((access & Opcodes.ACC_MANDATED) != 0) {
            buf.append("mandated ");
        }
        if ((access & Opcodes.ACC_ENUM) != 0) {
            buf.append("enum ");
        }
    }

    private void appendComa(final int i) {
        if (i != 0) {
            buf.append(", ");
        }
    }

    private void appendTypeReference(final int typeRef) {
        TypeReference ref = new TypeReference(typeRef);
        switch (ref.getSort()) {
            case TypeReference.CLASS_TYPE_PARAMETER:
                buf.append("CLASS_TYPE_PARAMETER ").append(
                        ref.getTypeParameterIndex());
                break;
            case TypeReference.METHOD_TYPE_PARAMETER:
                buf.append("METHOD_TYPE_PARAMETER ").append(
                        ref.getTypeParameterIndex());
                break;
            case TypeReference.CLASS_EXTENDS:
                buf.append("CLASS_EXTENDS ").append(ref.getSuperTypeIndex());
                break;
            case TypeReference.CLASS_TYPE_PARAMETER_BOUND:
                buf.append("CLASS_TYPE_PARAMETER_BOUND ")
                        .append(ref.getTypeParameterIndex()).append(", ")
                        .append(ref.getTypeParameterBoundIndex());
                break;
            case TypeReference.METHOD_TYPE_PARAMETER_BOUND:
                buf.append("METHOD_TYPE_PARAMETER_BOUND ")
                        .append(ref.getTypeParameterIndex()).append(", ")
                        .append(ref.getTypeParameterBoundIndex());
                break;
            case TypeReference.FIELD:
                buf.append("FIELD");
                break;
            case TypeReference.METHOD_RETURN:
                buf.append("METHOD_RETURN");
                break;
            case TypeReference.METHOD_RECEIVER:
                buf.append("METHOD_RECEIVER");
                break;
            case TypeReference.METHOD_FORMAL_PARAMETER:
                buf.append("METHOD_FORMAL_PARAMETER ").append(
                        ref.getFormalParameterIndex());
                break;
            case TypeReference.THROWS:
                buf.append("THROWS ").append(ref.getExceptionIndex());
                break;
            case TypeReference.LOCAL_VARIABLE:
                buf.append("LOCAL_VARIABLE");
                break;
            case TypeReference.RESOURCE_VARIABLE:
                buf.append("RESOURCE_VARIABLE");
                break;
            case TypeReference.EXCEPTION_PARAMETER:
                buf.append("EXCEPTION_PARAMETER ").append(
                        ref.getTryCatchBlockIndex());
                break;
            case TypeReference.INSTANCEOF:
                buf.append("INSTANCEOF");
                break;
            case TypeReference.NEW:
                buf.append("NEW");
                break;
            case TypeReference.CONSTRUCTOR_REFERENCE:
                buf.append("CONSTRUCTOR_REFERENCE");
                break;
            case TypeReference.METHOD_REFERENCE:
                buf.append("METHOD_REFERENCE");
                break;
            case TypeReference.CAST:
                buf.append("CAST ").append(ref.getTypeArgumentIndex());
                break;
            case TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT:
                buf.append("CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT ").append(
                        ref.getTypeArgumentIndex());
                break;
            case TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT:
                buf.append("METHOD_INVOCATION_TYPE_ARGUMENT ").append(
                        ref.getTypeArgumentIndex());
                break;
            case TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT:
                buf.append("CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT ").append(
                        ref.getTypeArgumentIndex());
                break;
            case TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT:
                buf.append("METHOD_REFERENCE_TYPE_ARGUMENT ").append(
                        ref.getTypeArgumentIndex());
                break;
        }
    }

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