// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source 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 name 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.
//
// 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.apache.tapestry5.internal.plastic.asm.util;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.tapestry5.internal.plastic.asm.Attribute;
import org.apache.tapestry5.internal.plastic.asm.ConstantDynamic;
import org.apache.tapestry5.internal.plastic.asm.Handle;
import org.apache.tapestry5.internal.plastic.asm.Label;
import org.apache.tapestry5.internal.plastic.asm.Opcodes;
import org.apache.tapestry5.internal.plastic.asm.Type;
import org.apache.tapestry5.internal.plastic.asm.TypePath;

/**
 * A {@link Printer} that prints the ASM code to generate the classes if visits.
 *
 * @author Eric Bruneton
 */
// DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility).
public class ASMifier extends Printer {

  /** The help message shown when command line arguments are incorrect. */
  private static final String USAGE =
      "Prints the ASM code to generate the given class.\n"
          + "Usage: ASMifier [-debug] <fully qualified class name or class file name>";

  /** A pseudo access flag used to distinguish class access flags. */
  private static final int ACCESS_CLASS = 0x40000;

  /** A pseudo access flag used to distinguish field access flags. */
  private static final int ACCESS_FIELD = 0x80000;

  /** A pseudo access flag used to distinguish inner class flags. */
  private static final int ACCESS_INNER = 0x100000;

  /** A pseudo access flag used to distinguish module requires / exports flags. */
  private static final int ACCESS_MODULE = 0x200000;

  private static final String ANNOTATION_VISITOR = "annotationVisitor";
  private static final String ANNOTATION_VISITOR0 = "annotationVisitor0 = ";
  private static final String COMMA = "\", \"";
  private static final String END_ARRAY = " });\n";
  private static final String END_PARAMETERS = ");\n\n";
  private static final String NEW_OBJECT_ARRAY = ", new Object[] {";
  private static final String VISIT_END = ".visitEnd();\n";

  private static final List<String> FRAME_TYPES =
      Collections.unmodifiableList(
          Arrays.asList(
              "Opcodes.TOP",
              "Opcodes.INTEGER",
              "Opcodes.FLOAT",
              "Opcodes.DOUBLE",
              "Opcodes.LONG",
              "Opcodes.NULL",
              "Opcodes.UNINITIALIZED_THIS"));

  private static final Map<Integer, String> CLASS_VERSIONS;

  static {
    HashMap<Integer, String> classVersions = new HashMap<>();
    classVersions.put(Opcodes.V1_1, "V1_1");
    classVersions.put(Opcodes.V1_2, "V1_2");
    classVersions.put(Opcodes.V1_3, "V1_3");
    classVersions.put(Opcodes.V1_4, "V1_4");
    classVersions.put(Opcodes.V1_5, "V1_5");
    classVersions.put(Opcodes.V1_6, "V1_6");
    classVersions.put(Opcodes.V1_7, "V1_7");
    classVersions.put(Opcodes.V1_8, "V1_8");
    classVersions.put(Opcodes.V9, "V9");
    classVersions.put(Opcodes.V10, "V10");
    classVersions.put(Opcodes.V11, "V11");
    classVersions.put(Opcodes.V12, "V12");
    classVersions.put(Opcodes.V13, "V13");
    classVersions.put(Opcodes.V14, "V14");
    classVersions.put(Opcodes.V15, "V15");
    CLASS_VERSIONS = Collections.unmodifiableMap(classVersions);
  }

  /** The name of the visitor variable in the produced code. */
  protected final String name;

  /** The identifier of the annotation visitor variable in the produced code. */
  protected final int id;

  /** The name of the Label variables in the produced code. */
  protected Map<Label, String> labelNames;

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

  /**
   * Constructs a new {@link ASMifier}.
   *
   * @param api the ASM API version implemented by this class. Must be one of {@link Opcodes#ASM4},
   *     {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7} or {@link Opcodes#ASM8}.
   * @param visitorVariableName the name of the visitor variable in the produced code.
   * @param annotationVisitorId identifier of the annotation visitor variable in the produced code.
   */
  protected ASMifier(
      final int api, final String visitorVariableName, final int annotationVisitorId) {
    super(api);
    this.name = visitorVariableName;
    this.id = annotationVisitorId;
  }

  /**
   * Prints the ASM source code to generate the given class to the standard output.
   *
   * <p>Usage: ASMifier [-debug] &lt;binary class name or class file name&gt;
   *
   * @param args the command line arguments.
   * @throws IOException if the class cannot be found, or if an IOException occurs.
   */
  public static void main(final String[] args) throws IOException {
    main(args, new PrintWriter(System.out, true), new PrintWriter(System.err, true));
  }

  /**
   * Prints the ASM source code to generate the given class to the given output.
   *
   * <p>Usage: ASMifier [-debug] &lt;binary class name or class file name&gt;
   *
   * @param args the command line arguments.
   * @param output where to print the result.
   * @param logger where to log errors.
   * @throws IOException if the class cannot be found, or if an IOException occurs.
   */
  static void main(final String[] args, final PrintWriter output, final PrintWriter logger)
      throws IOException {
    main(args, USAGE, new ASMifier(), output, logger);
  }

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

  @Override
  public void visit(
      final int version,
      final int access,
      final String name,
      final String signature,
      final String superName,
      final String[] interfaces) {
    String simpleName;
    if (name == null) {
      simpleName = "module-info";
    } else {
      int lastSlashIndex = name.lastIndexOf('/');
      if (lastSlashIndex == -1) {
        simpleName = name;
      } else {
        text.add("package asm." + name.substring(0, lastSlashIndex).replace('/', '.') + ";\n");
        simpleName = name.substring(lastSlashIndex + 1).replaceAll("[-\\(\\)]", "_");
      }
    }
    text.add("import org.apache.tapestry5.internal.plastic.asm.AnnotationVisitor;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.Attribute;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.ClassReader;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.ClassWriter;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.ConstantDynamic;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.FieldVisitor;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.Handle;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.Label;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.MethodVisitor;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.Opcodes;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.RecordComponentVisitor;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.Type;\n");
    text.add("import org.apache.tapestry5.internal.plastic.asm.TypePath;\n");
    text.add("public class " + simpleName + "Dump implements Opcodes {\n\n");
    text.add("public static byte[] dump () throws Exception {\n\n");
    text.add("ClassWriter classWriter = new ClassWriter(0);\n");
    text.add("FieldVisitor fieldVisitor;\n");
    text.add("RecordComponentVisitor recordComponentVisitor;\n");
    text.add("MethodVisitor methodVisitor;\n");
    text.add("AnnotationVisitor annotationVisitor0;\n\n");

    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visit(");
    String versionString = CLASS_VERSIONS.get(version);
    if (versionString != null) {
      stringBuilder.append(versionString);
    } else {
      stringBuilder.append(version);
    }
    stringBuilder.append(", ");
    appendAccessFlags(access | ACCESS_CLASS);
    stringBuilder.append(", ");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(signature);
    stringBuilder.append(", ");
    appendConstant(superName);
    stringBuilder.append(", ");
    if (interfaces != null && interfaces.length > 0) {
      stringBuilder.append("new String[] {");
      for (int i = 0; i < interfaces.length; ++i) {
        stringBuilder.append(i == 0 ? " " : ", ");
        appendConstant(interfaces[i]);
      }
      stringBuilder.append(" }");
    } else {
      stringBuilder.append("null");
    }
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitSource(final String file, final String debug) {
    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visitSource(");
    appendConstant(file);
    stringBuilder.append(", ");
    appendConstant(debug);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  @Override
  public Printer visitModule(final String name, final int flags, final String version) {
    stringBuilder.setLength(0);
    stringBuilder.append("ModuleVisitor moduleVisitor = classWriter.visitModule(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendAccessFlags(flags | ACCESS_MODULE);
    stringBuilder.append(", ");
    appendConstant(version);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier("moduleVisitor", 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  @Override
  public void visitNestHost(final String nestHost) {
    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visitNestHost(");
    appendConstant(nestHost);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitOuterClass(final String owner, final String name, final String descriptor) {
    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visitOuterClass(");
    appendConstant(owner);
    stringBuilder.append(", ");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  @Override
  public ASMifier visitClassAnnotation(final String descriptor, final boolean visible) {
    return visitAnnotation(descriptor, visible);
  }

  @Override
  public ASMifier visitClassTypeAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation(typeRef, typePath, descriptor, visible);
  }

  @Override
  public void visitClassAttribute(final Attribute attribute) {
    visitAttribute(attribute);
  }

  @Override
  public void visitNestMember(final String nestMember) {
    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visitNestMember(");
    appendConstant(nestMember);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  /**
   * <b>Experimental, use at your own risk.</b>.
   *
   * @param permittedSubtype the internal name of a permitted subtype.
   * @deprecated this API is experimental.
   */
  @Override
  @Deprecated
  public void visitPermittedSubtypeExperimental(final String permittedSubtype) {
    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visitPermittedSubtypeExperimental(");
    appendConstant(permittedSubtype);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitInnerClass(
      final String name, final String outerName, final String innerName, final int access) {
    stringBuilder.setLength(0);
    stringBuilder.append("classWriter.visitInnerClass(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(outerName);
    stringBuilder.append(", ");
    appendConstant(innerName);
    stringBuilder.append(", ");
    appendAccessFlags(access | ACCESS_INNER);
    stringBuilder.append(END_PARAMETERS);
    text.add(stringBuilder.toString());
  }

  @Override
  public ASMifier visitRecordComponent(
      final String name, final String descriptor, final String signature) {
    stringBuilder.setLength(0);
    stringBuilder.append("{\n");
    stringBuilder.append("recordComponentVisitor = classWriter.visitRecordComponent(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    appendConstant(signature);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier("recordComponentVisitor", 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  @Override
  public ASMifier visitField(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final Object value) {
    stringBuilder.setLength(0);
    stringBuilder.append("{\n");
    stringBuilder.append("fieldVisitor = classWriter.visitField(");
    appendAccessFlags(access | ACCESS_FIELD);
    stringBuilder.append(", ");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    appendConstant(signature);
    stringBuilder.append(", ");
    appendConstant(value);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier("fieldVisitor", 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  @Override
  public ASMifier visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    stringBuilder.setLength(0);
    stringBuilder.append("{\n");
    stringBuilder.append("methodVisitor = classWriter.visitMethod(");
    appendAccessFlags(access);
    stringBuilder.append(", ");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    appendConstant(signature);
    stringBuilder.append(", ");
    if (exceptions != null && exceptions.length > 0) {
      stringBuilder.append("new String[] {");
      for (int i = 0; i < exceptions.length; ++i) {
        stringBuilder.append(i == 0 ? " " : ", ");
        appendConstant(exceptions[i]);
      }
      stringBuilder.append(" }");
    } else {
      stringBuilder.append("null");
    }
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier("methodVisitor", 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

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

  // -----------------------------------------------------------------------------------------------
  // Modules
  // -----------------------------------------------------------------------------------------------

  @Override
  public void visitMainClass(final String mainClass) {
    stringBuilder.setLength(0);
    stringBuilder.append("moduleVisitor.visitMainClass(");
    appendConstant(mainClass);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitPackage(final String packaze) {
    stringBuilder.setLength(0);
    stringBuilder.append("moduleVisitor.visitPackage(");
    appendConstant(packaze);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitRequire(final String module, final int access, final String version) {
    stringBuilder.setLength(0);
    stringBuilder.append("moduleVisitor.visitRequire(");
    appendConstant(module);
    stringBuilder.append(", ");
    appendAccessFlags(access | ACCESS_MODULE);
    stringBuilder.append(", ");
    appendConstant(version);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitExport(final String packaze, final int access, final String... modules) {
    visitExportOrOpen("moduleVisitor.visitExport(", packaze, access, modules);
  }

  @Override
  public void visitOpen(final String packaze, final int access, final String... modules) {
    visitExportOrOpen("moduleVisitor.visitOpen(", packaze, access, modules);
  }

  private void visitExportOrOpen(
      final String visitMethod, final String packaze, final int access, final String... modules) {
    stringBuilder.setLength(0);
    stringBuilder.append(visitMethod);
    appendConstant(packaze);
    stringBuilder.append(", ");
    appendAccessFlags(access | ACCESS_MODULE);
    if (modules != null && modules.length > 0) {
      stringBuilder.append(", new String[] {");
      for (int i = 0; i < modules.length; ++i) {
        stringBuilder.append(i == 0 ? " " : ", ");
        appendConstant(modules[i]);
      }
      stringBuilder.append(" }");
    }
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitUse(final String service) {
    stringBuilder.setLength(0);
    stringBuilder.append("moduleVisitor.visitUse(");
    appendConstant(service);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitProvide(final String service, final String... providers) {
    stringBuilder.setLength(0);
    stringBuilder.append("moduleVisitor.visitProvide(");
    appendConstant(service);
    stringBuilder.append(",  new String[] {");
    for (int i = 0; i < providers.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ");
      appendConstant(providers[i]);
    }
    stringBuilder.append(END_ARRAY);
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitModuleEnd() {
    text.add("moduleVisitor.visitEnd();\n");
  }

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

  // DontCheck(OverloadMethodsDeclarationOrder): overloads are semantically different.
  @Override
  public void visit(final String name, final Object value) {
    stringBuilder.setLength(0);
    stringBuilder.append(ANNOTATION_VISITOR).append(id).append(".visit(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(value);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitEnum(final String name, final String descriptor, final String value) {
    stringBuilder.setLength(0);
    stringBuilder.append(ANNOTATION_VISITOR).append(id).append(".visitEnum(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    appendConstant(value);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public ASMifier visitAnnotation(final String name, final String descriptor) {
    stringBuilder.setLength(0);
    stringBuilder
        .append("{\n")
        .append("AnnotationVisitor annotationVisitor")
        .append(id + 1)
        .append(" = annotationVisitor");
    stringBuilder.append(id).append(".visitAnnotation(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, id + 1);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  @Override
  public ASMifier visitArray(final String name) {
    stringBuilder.setLength(0);
    stringBuilder.append("{\n");
    stringBuilder
        .append("AnnotationVisitor annotationVisitor")
        .append(id + 1)
        .append(" = annotationVisitor");
    stringBuilder.append(id).append(".visitArray(");
    appendConstant(name);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, id + 1);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  @Override
  public void visitAnnotationEnd() {
    stringBuilder.setLength(0);
    stringBuilder.append(ANNOTATION_VISITOR).append(id).append(VISIT_END);
    text.add(stringBuilder.toString());
  }

  // -----------------------------------------------------------------------------------------------
  // Record components
  // -----------------------------------------------------------------------------------------------

  @Override
  public ASMifier visitRecordComponentAnnotation(final String descriptor, final boolean visible) {
    return visitAnnotation(descriptor, visible);
  }

  @Override
  public ASMifier visitRecordComponentTypeAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation(typeRef, typePath, descriptor, visible);
  }

  @Override
  public void visitRecordComponentAttribute(final Attribute attribute) {
    visitAttribute(attribute);
  }

  @Override
  public void visitRecordComponentEnd() {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(VISIT_END);
    text.add(stringBuilder.toString());
  }

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

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

  @Override
  public ASMifier visitFieldTypeAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation(typeRef, typePath, descriptor, visible);
  }

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

  @Override
  public void visitFieldEnd() {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(VISIT_END);
    text.add(stringBuilder.toString());
  }

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

  @Override
  public void visitParameter(final String parameterName, final int access) {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(".visitParameter(");
    appendString(stringBuilder, parameterName);
    stringBuilder.append(", ");
    appendAccessFlags(access);
    text.add(stringBuilder.append(");\n").toString());
  }

  @Override
  public ASMifier visitAnnotationDefault() {
    stringBuilder.setLength(0);
    stringBuilder
        .append("{\n")
        .append(ANNOTATION_VISITOR0)
        .append(name)
        .append(".visitAnnotationDefault();\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

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

  @Override
  public ASMifier visitMethodTypeAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation(typeRef, typePath, descriptor, visible);
  }

  @Override
  public ASMifier visitAnnotableParameterCount(final int parameterCount, final boolean visible) {
    stringBuilder.setLength(0);
    stringBuilder
        .append(name)
        .append(".visitAnnotableParameterCount(")
        .append(parameterCount)
        .append(", ")
        .append(visible)
        .append(");\n");
    text.add(stringBuilder.toString());
    return this;
  }

  @Override
  public ASMifier visitParameterAnnotation(
      final int parameter, final String descriptor, final boolean visible) {
    stringBuilder.setLength(0);
    stringBuilder
        .append("{\n")
        .append(ANNOTATION_VISITOR0)
        .append(name)
        .append(".visitParameterAnnotation(")
        .append(parameter)
        .append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ").append(visible).append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  @Override
  public void visitMethodAttribute(final Attribute attribute) {
    visitAttribute(attribute);
  }

  @Override
  public void visitCode() {
    text.add(name + ".visitCode();\n");
  }

  @Override
  public void visitFrame(
      final int type,
      final int numLocal,
      final Object[] local,
      final int numStack,
      final Object[] stack) {
    stringBuilder.setLength(0);
    switch (type) {
      case Opcodes.F_NEW:
      case Opcodes.F_FULL:
        declareFrameTypes(numLocal, local);
        declareFrameTypes(numStack, stack);
        if (type == Opcodes.F_NEW) {
          stringBuilder.append(name).append(".visitFrame(Opcodes.F_NEW, ");
        } else {
          stringBuilder.append(name).append(".visitFrame(Opcodes.F_FULL, ");
        }
        stringBuilder.append(numLocal).append(NEW_OBJECT_ARRAY);
        appendFrameTypes(numLocal, local);
        stringBuilder.append("}, ").append(numStack).append(NEW_OBJECT_ARRAY);
        appendFrameTypes(numStack, stack);
        stringBuilder.append('}');
        break;
      case Opcodes.F_APPEND:
        declareFrameTypes(numLocal, local);
        stringBuilder
            .append(name)
            .append(".visitFrame(Opcodes.F_APPEND,")
            .append(numLocal)
            .append(NEW_OBJECT_ARRAY);
        appendFrameTypes(numLocal, local);
        stringBuilder.append("}, 0, null");
        break;
      case Opcodes.F_CHOP:
        stringBuilder
            .append(name)
            .append(".visitFrame(Opcodes.F_CHOP,")
            .append(numLocal)
            .append(", null, 0, null");
        break;
      case Opcodes.F_SAME:
        stringBuilder.append(name).append(".visitFrame(Opcodes.F_SAME, 0, null, 0, null");
        break;
      case Opcodes.F_SAME1:
        declareFrameTypes(1, stack);
        stringBuilder
            .append(name)
            .append(".visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {");
        appendFrameTypes(1, stack);
        stringBuilder.append('}');
        break;
      default:
        throw new IllegalArgumentException();
    }
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

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

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

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

  @Override
  public void visitTypeInsn(final int opcode, final String type) {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(".visitTypeInsn(").append(OPCODES[opcode]).append(", ");
    appendConstant(type);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitFieldInsn(
      final int opcode, final String owner, final String name, final String descriptor) {
    stringBuilder.setLength(0);
    stringBuilder.append(this.name).append(".visitFieldInsn(").append(OPCODES[opcode]).append(", ");
    appendConstant(owner);
    stringBuilder.append(", ");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitMethodInsn(
      final int opcode,
      final String owner,
      final String name,
      final String descriptor,
      final boolean isInterface) {
    stringBuilder.setLength(0);
    stringBuilder
        .append(this.name)
        .append(".visitMethodInsn(")
        .append(OPCODES[opcode])
        .append(", ");
    appendConstant(owner);
    stringBuilder.append(", ");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    stringBuilder.append(isInterface ? "true" : "false");
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitInvokeDynamicInsn(
      final String name,
      final String descriptor,
      final Handle bootstrapMethodHandle,
      final Object... bootstrapMethodArguments) {
    stringBuilder.setLength(0);
    stringBuilder.append(this.name).append(".visitInvokeDynamicInsn(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    appendConstant(bootstrapMethodHandle);
    stringBuilder.append(", new Object[]{");
    for (int i = 0; i < bootstrapMethodArguments.length; ++i) {
      appendConstant(bootstrapMethodArguments[i]);
      if (i != bootstrapMethodArguments.length - 1) {
        stringBuilder.append(", ");
      }
    }
    stringBuilder.append("});\n");
    text.add(stringBuilder.toString());
  }

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

  @Override
  public void visitLabel(final Label label) {
    stringBuilder.setLength(0);
    declareLabel(label);
    stringBuilder.append(name).append(".visitLabel(");
    appendLabel(label);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitLdcInsn(final Object value) {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(".visitLdcInsn(");
    appendConstant(value);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

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

  @Override
  public void visitTableSwitchInsn(
      final int min, final int max, final Label dflt, final Label... labels) {
    stringBuilder.setLength(0);
    for (Label label : labels) {
      declareLabel(label);
    }
    declareLabel(dflt);

    stringBuilder
        .append(name)
        .append(".visitTableSwitchInsn(")
        .append(min)
        .append(", ")
        .append(max)
        .append(", ");
    appendLabel(dflt);
    stringBuilder.append(", new Label[] {");
    for (int i = 0; i < labels.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ");
      appendLabel(labels[i]);
    }
    stringBuilder.append(END_ARRAY);
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
    stringBuilder.setLength(0);
    for (Label label : labels) {
      declareLabel(label);
    }
    declareLabel(dflt);

    stringBuilder.append(name).append(".visitLookupSwitchInsn(");
    appendLabel(dflt);
    stringBuilder.append(", new int[] {");
    for (int i = 0; i < keys.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ").append(keys[i]);
    }
    stringBuilder.append(" }, new Label[] {");
    for (int i = 0; i < labels.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ");
      appendLabel(labels[i]);
    }
    stringBuilder.append(END_ARRAY);
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(".visitMultiANewArrayInsn(");
    appendConstant(descriptor);
    stringBuilder.append(", ").append(numDimensions).append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public ASMifier visitInsnAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation("visitInsnAnnotation", typeRef, typePath, descriptor, visible);
  }

  @Override
  public void visitTryCatchBlock(
      final Label start, final Label end, final Label handler, final String type) {
    stringBuilder.setLength(0);
    declareLabel(start);
    declareLabel(end);
    declareLabel(handler);
    stringBuilder.append(name).append(".visitTryCatchBlock(");
    appendLabel(start);
    stringBuilder.append(", ");
    appendLabel(end);
    stringBuilder.append(", ");
    appendLabel(handler);
    stringBuilder.append(", ");
    appendConstant(type);
    stringBuilder.append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public ASMifier visitTryCatchAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation("visitTryCatchAnnotation", typeRef, typePath, descriptor, visible);
  }

  @Override
  public void visitLocalVariable(
      final String name,
      final String descriptor,
      final String signature,
      final Label start,
      final Label end,
      final int index) {
    stringBuilder.setLength(0);
    stringBuilder.append(this.name).append(".visitLocalVariable(");
    appendConstant(name);
    stringBuilder.append(", ");
    appendConstant(descriptor);
    stringBuilder.append(", ");
    appendConstant(signature);
    stringBuilder.append(", ");
    appendLabel(start);
    stringBuilder.append(", ");
    appendLabel(end);
    stringBuilder.append(", ").append(index).append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public Printer visitLocalVariableAnnotation(
      final int typeRef,
      final TypePath typePath,
      final Label[] start,
      final Label[] end,
      final int[] index,
      final String descriptor,
      final boolean visible) {
    stringBuilder.setLength(0);
    stringBuilder
        .append("{\n")
        .append(ANNOTATION_VISITOR0)
        .append(name)
        .append(".visitLocalVariableAnnotation(")
        .append(typeRef);
    if (typePath == null) {
      stringBuilder.append(", null, ");
    } else {
      stringBuilder.append(", TypePath.fromString(\"").append(typePath).append("\"), ");
    }
    stringBuilder.append("new Label[] {");
    for (int i = 0; i < start.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ");
      appendLabel(start[i]);
    }
    stringBuilder.append(" }, new Label[] {");
    for (int i = 0; i < end.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ");
      appendLabel(end[i]);
    }
    stringBuilder.append(" }, new int[] {");
    for (int i = 0; i < index.length; ++i) {
      stringBuilder.append(i == 0 ? " " : ", ").append(index[i]);
    }
    stringBuilder.append(" }, ");
    appendConstant(descriptor);
    stringBuilder.append(", ").append(visible).append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

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

  @Override
  public void visitMaxs(final int maxStack, final int maxLocals) {
    stringBuilder.setLength(0);
    stringBuilder
        .append(name)
        .append(".visitMaxs(")
        .append(maxStack)
        .append(", ")
        .append(maxLocals)
        .append(");\n");
    text.add(stringBuilder.toString());
  }

  @Override
  public void visitMethodEnd() {
    stringBuilder.setLength(0);
    stringBuilder.append(name).append(VISIT_END);
    text.add(stringBuilder.toString());
  }

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

  /**
   * Visits a class, field or method annotation.
   *
   * @param descriptor the class descriptor of the annotation class.
   * @param visible {@literal true} if the annotation is visible at runtime.
   * @return a new {@link ASMifier} to visit the annotation values.
   */
  // DontCheck(OverloadMethodsDeclarationOrder): overloads are semantically different.
  public ASMifier visitAnnotation(final String descriptor, final boolean visible) {
    stringBuilder.setLength(0);
    stringBuilder
        .append("{\n")
        .append(ANNOTATION_VISITOR0)
        .append(name)
        .append(".visitAnnotation(");
    appendConstant(descriptor);
    stringBuilder.append(", ").append(visible).append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  /**
   * Visits a class, field or method type annotation.
   *
   * @param typeRef a reference to the annotated type. The sort of this type reference must be
   *     {@link org.apache.tapestry5.internal.plastic.asm.TypeReference#FIELD}. See {@link org.apache.tapestry5.internal.plastic.asm.TypeReference}.
   * @param typePath the path to the annotated type argument, wildcard bound, array element type, or
   *     static inner type within 'typeRef'. May be {@literal null} if the annotation targets
   *     'typeRef' as a whole.
   * @param descriptor the class descriptor of the annotation class.
   * @param visible {@literal true} if the annotation is visible at runtime.
   * @return a new {@link ASMifier} to visit the annotation values.
   */
  public ASMifier visitTypeAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    return visitTypeAnnotation("visitTypeAnnotation", typeRef, typePath, descriptor, visible);
  }

  /**
   * Visits a class, field, method, instruction or try catch block type annotation.
   *
   * @param method the name of the visit method for this type of annotation.
   * @param typeRef a reference to the annotated type. The sort of this type reference must be
   *     {@link org.apache.tapestry5.internal.plastic.asm.TypeReference#FIELD}. See {@link org.apache.tapestry5.internal.plastic.asm.TypeReference}.
   * @param typePath the path to the annotated type argument, wildcard bound, array element type, or
   *     static inner type within 'typeRef'. May be {@literal null} if the annotation targets
   *     'typeRef' as a whole.
   * @param descriptor the class descriptor of the annotation class.
   * @param visible {@literal true} if the annotation is visible at runtime.
   * @return a new {@link ASMifier} to visit the annotation values.
   */
  public ASMifier visitTypeAnnotation(
      final String method,
      final int typeRef,
      final TypePath typePath,
      final String descriptor,
      final boolean visible) {
    stringBuilder.setLength(0);
    stringBuilder
        .append("{\n")
        .append(ANNOTATION_VISITOR0)
        .append(name)
        .append(".")
        .append(method)
        .append("(")
        .append(typeRef);
    if (typePath == null) {
      stringBuilder.append(", null, ");
    } else {
      stringBuilder.append(", TypePath.fromString(\"").append(typePath).append("\"), ");
    }
    appendConstant(descriptor);
    stringBuilder.append(", ").append(visible).append(");\n");
    text.add(stringBuilder.toString());
    ASMifier asmifier = createASMifier(ANNOTATION_VISITOR, 0);
    text.add(asmifier.getText());
    text.add("}\n");
    return asmifier;
  }

  /**
   * Visit a class, field or method attribute.
   *
   * @param attribute an attribute.
   */
  public void visitAttribute(final Attribute attribute) {
    stringBuilder.setLength(0);
    stringBuilder.append("// ATTRIBUTE ").append(attribute.type).append('\n');
    if (attribute instanceof ASMifierSupport) {
      if (labelNames == null) {
        labelNames = new HashMap<>();
      }
      stringBuilder.append("{\n");
      ((ASMifierSupport) attribute).asmify(stringBuilder, "attribute", labelNames);
      stringBuilder.append(name).append(".visitAttribute(attribute);\n");
      stringBuilder.append("}\n");
    }
    text.add(stringBuilder.toString());
  }

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

  /**
   * Constructs a new {@link ASMifier}.
   *
   * @param visitorVariableName the name of the visitor variable in the produced code.
   * @param annotationVisitorId identifier of the annotation visitor variable in the produced code.
   * @return a new {@link ASMifier}.
   */
  // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility).
  protected ASMifier createASMifier(
      final String visitorVariableName, final int annotationVisitorId) {
    return new ASMifier(api, visitorVariableName, annotationVisitorId);
  }

  /**
   * Appends a string representation of the given access flags to {@link #stringBuilder}.
   *
   * @param accessFlags some access flags.
   */
  private void appendAccessFlags(final int accessFlags) {
    boolean isEmpty = true;
    if ((accessFlags & Opcodes.ACC_PUBLIC) != 0) {
      stringBuilder.append("ACC_PUBLIC");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_PRIVATE) != 0) {
      stringBuilder.append("ACC_PRIVATE");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_PROTECTED) != 0) {
      stringBuilder.append("ACC_PROTECTED");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_FINAL) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      if ((accessFlags & ACCESS_MODULE) == 0) {
        stringBuilder.append("ACC_FINAL");
      } else {
        stringBuilder.append("ACC_TRANSITIVE");
      }
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_STATIC) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_STATIC");
      isEmpty = false;
    }
    if ((accessFlags & (Opcodes.ACC_SYNCHRONIZED | Opcodes.ACC_SUPER | Opcodes.ACC_TRANSITIVE))
        != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      if ((accessFlags & ACCESS_CLASS) == 0) {
        if ((accessFlags & ACCESS_MODULE) == 0) {
          stringBuilder.append("ACC_SYNCHRONIZED");
        } else {
          stringBuilder.append("ACC_TRANSITIVE");
        }
      } else {
        stringBuilder.append("ACC_SUPER");
      }
      isEmpty = false;
    }
    if ((accessFlags & (Opcodes.ACC_VOLATILE | Opcodes.ACC_BRIDGE | Opcodes.ACC_STATIC_PHASE))
        != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      if ((accessFlags & ACCESS_FIELD) == 0) {
        if ((accessFlags & ACCESS_MODULE) == 0) {
          stringBuilder.append("ACC_BRIDGE");
        } else {
          stringBuilder.append("ACC_STATIC_PHASE");
        }
      } else {
        stringBuilder.append("ACC_VOLATILE");
      }
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_VARARGS) != 0
        && (accessFlags & (ACCESS_CLASS | ACCESS_FIELD)) == 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_VARARGS");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_TRANSIENT) != 0 && (accessFlags & ACCESS_FIELD) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_TRANSIENT");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_NATIVE) != 0
        && (accessFlags & (ACCESS_CLASS | ACCESS_FIELD)) == 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_NATIVE");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_ENUM) != 0
        && (accessFlags & (ACCESS_CLASS | ACCESS_FIELD | ACCESS_INNER)) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_ENUM");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_ANNOTATION) != 0
        && (accessFlags & (ACCESS_CLASS | ACCESS_INNER)) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_ANNOTATION");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_ABSTRACT) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_ABSTRACT");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_INTERFACE) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_INTERFACE");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_STRICT) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_STRICT");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_SYNTHETIC");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_DEPRECATED");
      isEmpty = false;
    }
    if ((accessFlags & Opcodes.ACC_RECORD) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      stringBuilder.append("ACC_RECORD");
      isEmpty = false;
    }
    if ((accessFlags & (Opcodes.ACC_MANDATED | Opcodes.ACC_MODULE)) != 0) {
      if (!isEmpty) {
        stringBuilder.append(" | ");
      }
      if ((accessFlags & ACCESS_CLASS) == 0) {
        stringBuilder.append("ACC_MANDATED");
      } else {
        stringBuilder.append("ACC_MODULE");
      }
      isEmpty = false;
    }
    if (isEmpty) {
      stringBuilder.append('0');
    }
  }

  /**
   * Appends a string representation of the given constant to {@link #stringBuilder}.
   *
   * @param value a {@link String}, {@link Type}, {@link Handle}, {@link Byte}, {@link Short},
   *     {@link Character}, {@link Integer}, {@link Float}, {@link Long} or {@link Double} object,
   *     or an array of primitive values. May be {@literal null}.
   */
  protected void appendConstant(final Object value) {
    if (value == null) {
      stringBuilder.append("null");
    } else if (value instanceof String) {
      appendString(stringBuilder, (String) value);
    } else if (value instanceof Type) {
      stringBuilder.append("Type.getType(\"");
      stringBuilder.append(((Type) value).getDescriptor());
      stringBuilder.append("\")");
    } else if (value instanceof Handle) {
      stringBuilder.append("new Handle(");
      Handle handle = (Handle) value;
      stringBuilder.append("Opcodes.").append(HANDLE_TAG[handle.getTag()]).append(", \"");
      stringBuilder.append(handle.getOwner()).append(COMMA);
      stringBuilder.append(handle.getName()).append(COMMA);
      stringBuilder.append(handle.getDesc()).append("\", ");
      stringBuilder.append(handle.isInterface()).append(")");
    } else if (value instanceof ConstantDynamic) {
      stringBuilder.append("new ConstantDynamic(\"");
      ConstantDynamic constantDynamic = (ConstantDynamic) value;
      stringBuilder.append(constantDynamic.getName()).append(COMMA);
      stringBuilder.append(constantDynamic.getDescriptor()).append("\", ");
      appendConstant(constantDynamic.getBootstrapMethod());
      stringBuilder.append(NEW_OBJECT_ARRAY);
      int bootstrapMethodArgumentCount = constantDynamic.getBootstrapMethodArgumentCount();
      for (int i = 0; i < bootstrapMethodArgumentCount; ++i) {
        appendConstant(constantDynamic.getBootstrapMethodArgument(i));
        if (i != bootstrapMethodArgumentCount - 1) {
          stringBuilder.append(", ");
        }
      }
      stringBuilder.append("})");
    } else if (value instanceof Byte) {
      stringBuilder.append("new Byte((byte)").append(value).append(')');
    } else if (value instanceof Boolean) {
      stringBuilder.append(((Boolean) value).booleanValue() ? "Boolean.TRUE" : "Boolean.FALSE");
    } else if (value instanceof Short) {
      stringBuilder.append("new Short((short)").append(value).append(')');
    } else if (value instanceof Character) {
      stringBuilder
          .append("new Character((char)")
          .append((int) ((Character) value).charValue())
          .append(')');
    } else if (value instanceof Integer) {
      stringBuilder.append("new Integer(").append(value).append(')');
    } else if (value instanceof Float) {
      stringBuilder.append("new Float(\"").append(value).append("\")");
    } else if (value instanceof Long) {
      stringBuilder.append("new Long(").append(value).append("L)");
    } else if (value instanceof Double) {
      stringBuilder.append("new Double(\"").append(value).append("\")");
    } else if (value instanceof byte[]) {
      byte[] byteArray = (byte[]) value;
      stringBuilder.append("new byte[] {");
      for (int i = 0; i < byteArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append(byteArray[i]);
      }
      stringBuilder.append('}');
    } else if (value instanceof boolean[]) {
      boolean[] booleanArray = (boolean[]) value;
      stringBuilder.append("new boolean[] {");
      for (int i = 0; i < booleanArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append(booleanArray[i]);
      }
      stringBuilder.append('}');
    } else if (value instanceof short[]) {
      short[] shortArray = (short[]) value;
      stringBuilder.append("new short[] {");
      for (int i = 0; i < shortArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append("(short)").append(shortArray[i]);
      }
      stringBuilder.append('}');
    } else if (value instanceof char[]) {
      char[] charArray = (char[]) value;
      stringBuilder.append("new char[] {");
      for (int i = 0; i < charArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append("(char)").append((int) charArray[i]);
      }
      stringBuilder.append('}');
    } else if (value instanceof int[]) {
      int[] intArray = (int[]) value;
      stringBuilder.append("new int[] {");
      for (int i = 0; i < intArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append(intArray[i]);
      }
      stringBuilder.append('}');
    } else if (value instanceof long[]) {
      long[] longArray = (long[]) value;
      stringBuilder.append("new long[] {");
      for (int i = 0; i < longArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append(longArray[i]).append('L');
      }
      stringBuilder.append('}');
    } else if (value instanceof float[]) {
      float[] floatArray = (float[]) value;
      stringBuilder.append("new float[] {");
      for (int i = 0; i < floatArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append(floatArray[i]).append('f');
      }
      stringBuilder.append('}');
    } else if (value instanceof double[]) {
      double[] doubleArray = (double[]) value;
      stringBuilder.append("new double[] {");
      for (int i = 0; i < doubleArray.length; i++) {
        stringBuilder.append(i == 0 ? "" : ",").append(doubleArray[i]).append('d');
      }
      stringBuilder.append('}');
    }
  }

  /**
   * Calls {@link #declareLabel} for each label in the given stack map frame types.
   *
   * @param numTypes the number of stack map frame types in 'frameTypes'.
   * @param frameTypes an array of stack map frame types, in the format described in {@link
   *     org.apache.tapestry5.internal.plastic.asm.MethodVisitor#visitFrame}.
   */
  private void declareFrameTypes(final int numTypes, final Object[] frameTypes) {
    for (int i = 0; i < numTypes; ++i) {
      if (frameTypes[i] instanceof Label) {
        declareLabel((Label) frameTypes[i]);
      }
    }
  }

  /**
   * Appends the given stack map frame types to {@link #stringBuilder}.
   *
   * @param numTypes the number of stack map frame types in 'frameTypes'.
   * @param frameTypes an array of stack map frame types, in the format described in {@link
   *     org.apache.tapestry5.internal.plastic.asm.MethodVisitor#visitFrame}.
   */
  private void appendFrameTypes(final int numTypes, final Object[] frameTypes) {
    for (int i = 0; i < numTypes; ++i) {
      if (i > 0) {
        stringBuilder.append(", ");
      }
      if (frameTypes[i] instanceof String) {
        appendConstant(frameTypes[i]);
      } else if (frameTypes[i] instanceof Integer) {
        stringBuilder.append(FRAME_TYPES.get(((Integer) frameTypes[i]).intValue()));
      } else {
        appendLabel((Label) frameTypes[i]);
      }
    }
  }

  /**
   * Appends a declaration of the given label to {@link #stringBuilder}. This declaration is of the
   * form "Label labelXXX = new Label();". Does nothing if the given label has already been
   * declared.
   *
   * @param label a label.
   */
  protected void declareLabel(final Label label) {
    if (labelNames == null) {
      labelNames = new HashMap<>();
    }
    String labelName = labelNames.get(label);
    if (labelName == null) {
      labelName = "label" + labelNames.size();
      labelNames.put(label, labelName);
      stringBuilder.append("Label ").append(labelName).append(" = new Label();\n");
    }
  }

  /**
   * Appends the name of the given label to {@link #stringBuilder}. The given label <i>must</i>
   * already have a name. One way to ensure this is to always call {@link #declareLabel} before
   * calling this method.
   *
   * @param label a label.
   */
  protected void appendLabel(final Label label) {
    stringBuilder.append(labelNames.get(label));
  }
}
