/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.javaagent.tooling.instrumentation.indy;

import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

/**
 * Transform inline advice to delegating advice. This transformation is best effort, it isn't able
 * to transform all the advices.
 */
class AdviceTransformer {
  private static final Type OBJECT_TYPE = Type.getType(Object.class);
  private static final Type OBJECT_ARRAY_TYPE = Type.getType(Object[].class);

  static byte[] transform(byte[] bytes) {
    ClassReader cr = new ClassReader(bytes);
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
    ClassNode classNode = new ClassNode();
    cr.accept(classNode, ClassReader.EXPAND_FRAMES);

    // skip the class if there aren't any methods with advice annotations or these annotations have
    // set inline = false
    if (!hasInlineAdvice(classNode)) {
      classNode.accept(cw);
      return cw.toByteArray();
    }

    // sort enter advice method before exit advice
    classNode.methods.sort(
        Comparator.comparingInt(
            (methodNode) -> {
              if (isEnterAdvice(methodNode)) {
                return 1;
              } else if (isExitAdvice(methodNode)) {
                return 2;
              }
              return 0;
            }));

    TransformationContext context = new TransformationContext();
    ClassVisitor cv =
        new ClassVisitor(AsmApi.VERSION, cw) {

          @Override
          public MethodVisitor visitMethod(
              int access, String name, String descriptor, String signature, String[] exceptions) {
            ClassVisitor classVisitor = this.cv;
            return new MethodNode(api, access, name, descriptor, signature, exceptions) {
              @Override
              public void visitEnd() {
                super.visitEnd();

                instrument(context, this, classVisitor);
              }
            };
          }
        };
    classNode.accept(cv);
    return cw.toByteArray();
  }

  private static boolean hasInlineAdvice(ClassNode classNode) {
    for (MethodNode mn : classNode.methods) {
      if (hasInlineAdvice(mn)) {
        return true;
      }
    }

    return false;
  }

  private static boolean hasInlineAdvice(MethodNode methodNode) {
    return hasInlineAdvice(methodNode, ADVICE_ON_METHOD_ENTER)
        || hasInlineAdvice(methodNode, ADVICE_ON_METHOD_EXIT);
  }

  private static boolean hasInlineAdvice(MethodNode methodNode, Type type) {
    AnnotationNode annotationNode = getAnnotationNode(methodNode, type);
    if (annotationNode != null) {
      // delegating advice has attribute "inline" = false
      // all other advice is inline
      return !Boolean.FALSE.equals(getAttributeValue(annotationNode, "inline"));
    }
    return false;
  }

  // method argument annotated with Advice.Argument or Advice.Return
  private static class OutputArgument {
    // index of the method argument with the annotation
    final int adviceIndex;
    // value of the annotation or -1 if Advice.Return or Advice.Enter
    final int methodIndex;

    OutputArgument(int adviceIndex, int methodIndex) {
      this.adviceIndex = adviceIndex;
      this.methodIndex = methodIndex;
    }
  }

  // method argument annotated with Advice.Local
  private static class AdviceLocal {
    // index of the method argument with the annotation
    final int adviceIndex;
    // value of the Advice.Local annotation
    final String name;

    AdviceLocal(int adviceIndex, String name) {
      this.adviceIndex = adviceIndex;
      this.name = name;
    }
  }

  private static final Type ADVICE_ARGUMENT = Type.getType(Advice.Argument.class);

  /** List of arguments annotated with {@code @Advice.Argument(readOnly = false)}. */
  private static List<OutputArgument> getWritableArguments(MethodNode source) {
    List<OutputArgument> result = new ArrayList<>();
    if (source.visibleParameterAnnotations != null) {
      int i = 0;
      for (List<AnnotationNode> list : source.visibleParameterAnnotations) {
        for (AnnotationNode annotationNode : list) {
          Type annotationType = Type.getType(annotationNode.desc);
          if (ADVICE_ARGUMENT.equals(annotationType) && isWriteable(annotationNode)) {
            Object value = getAnnotationValue(annotationNode);
            if (value instanceof Integer) {
              result.add(new OutputArgument(i, (Integer) value));
            }
          }
        }
        i++;
      }
    }

    return result;
  }

  private static final Type ADVICE_RETURN = Type.getType(Advice.Return.class);

  /** Argument annotated with {@code @Advice.Return(readOnly = false)} or {@code null}. */
  private static OutputArgument getWritableReturnValue(MethodNode source) {
    if (source.visibleParameterAnnotations != null) {
      int i = 0;
      for (List<AnnotationNode> list : source.visibleParameterAnnotations) {
        for (AnnotationNode annotationNode : list) {
          Type annotationType = Type.getType(annotationNode.desc);
          if (ADVICE_RETURN.equals(annotationType) && isWriteable(annotationNode)) {
            return new OutputArgument(i, -1);
          }
        }
        i++;
      }
    }

    return null;
  }

  private static final Type ADVICE_ENTER = Type.getType(Advice.Enter.class);

  /** Argument annotated with {@code @Advice.Enter} or {@code null}. */
  private static OutputArgument getEnterArgument(MethodNode source) {
    Type[] argumentTypes = Type.getArgumentTypes(source.desc);
    if (source.visibleParameterAnnotations != null) {
      int i = 0;
      for (List<AnnotationNode> list : source.visibleParameterAnnotations) {
        for (AnnotationNode annotationNode : list) {
          Type annotationType = Type.getType(annotationNode.desc);
          if (ADVICE_ENTER.equals(annotationType)
              && argumentTypes[i].getDescriptor().length() > 1) {
            return new OutputArgument(i, -1);
          }
        }
        i++;
      }
    }

    return null;
  }

  private static final Type ADVICE_LOCAL = Type.getType(Advice.Local.class);

  /** List of arguments annotated with {@code @Advice.Local}. */
  private static List<AdviceLocal> getLocals(MethodNode source) {
    List<AdviceLocal> result = new ArrayList<>();
    if (source.visibleParameterAnnotations != null) {
      int i = 0;
      for (List<AnnotationNode> list : source.visibleParameterAnnotations) {
        for (AnnotationNode annotationNode : list) {
          Type annotationType = Type.getType(annotationNode.desc);
          if (ADVICE_LOCAL.equals(annotationType)) {
            Object value = getAnnotationValue(annotationNode);
            if (value instanceof String) {
              result.add(new AdviceLocal(i, (String) value));
            }
          }
        }
        i++;
      }
    }

    return result;
  }

  private static final Type ADVICE_ON_METHOD_ENTER = Type.getType(Advice.OnMethodEnter.class);

  private static boolean isEnterAdvice(MethodNode source) {
    return hasAnnotation(source, ADVICE_ON_METHOD_ENTER);
  }

  private static final Type ADVICE_ON_METHOD_EXIT = Type.getType(Advice.OnMethodExit.class);

  private static boolean isExitAdvice(MethodNode source) {
    return hasAnnotation(source, ADVICE_ON_METHOD_EXIT);
  }

  private static AnnotationNode getAnnotationNode(MethodNode source, Type type) {
    if (source.visibleAnnotations != null) {
      for (AnnotationNode annotationNode : source.visibleAnnotations) {
        Type annotationType = Type.getType(annotationNode.desc);
        if (type.equals(annotationType)) {
          return annotationNode;
        }
      }
    }

    return null;
  }

  private static boolean hasAnnotation(MethodNode source, Type type) {
    return getAnnotationNode(source, type) != null;
  }

  /**
   * Transform arguments annotated with {@code @Advice.Argument(readOnly = false)}.
   *
   * <pre>{@code
   * void foo(@Advice.Argument(value = 0, readOnly = false) T1 foo, @Advice.Argument(value = 0, readOnly = false) T2 bar)
   * }</pre>
   *
   * <p>is transformed to
   *
   * <pre>{@code
   * @Advice.AssignReturned.ToArguments({
   *   @Advice.AssignReturned.ToArguments.ToArgument(value = 0, index = 0, typing = DYNAMIC),
   *   @Advice.AssignReturned.ToArguments.ToArgument(value = 1, index = 1, typing = DYNAMIC)
   * })
   * void foo(@Advice.Argument(value = 0, readOnly = true) T1 foo, @Advice.Argument(value = 0, readOnly = true) T2 bar) {
   *   ...
   *   Object[] result = new Object[2];
   *   result[0] = foo;
   *   result[1] = bar;
   *   return result;
   * }
   * }</pre>
   */
  private static MethodVisitor instrumentWritableArguments(
      MethodVisitor target,
      MethodNode source,
      List<OutputArgument> writableArguments,
      int returnIndex) {
    MethodVisitor result =
        new MethodVisitor(AsmApi.VERSION, target) {
          @Override
          public void visitCode() {
            AnnotationVisitor av =
                visitAnnotation(Type.getDescriptor(Advice.AssignReturned.ToArguments.class), true);
            AnnotationVisitor valueArrayVisitor = av.visitArray("value");
            for (int i = 0; i < writableArguments.size(); i++) {
              OutputArgument argument = writableArguments.get(i);
              AnnotationVisitor valueVisitor =
                  valueArrayVisitor.visitAnnotation(
                      null, Type.getDescriptor(Advice.AssignReturned.ToArguments.ToArgument.class));
              valueVisitor.visit("value", argument.methodIndex);
              valueVisitor.visit("index", returnIndex + i);
              valueVisitor.visitEnum(
                  "typing", Type.getDescriptor(Assigner.Typing.class), "DYNAMIC");
              valueVisitor.visitEnd();
            }
            valueArrayVisitor.visitEnd();
            av.visitEnd();
            super.visitCode();
          }

          @Override
          public void visitInsn(int opcode) {
            if (Opcodes.ARETURN == opcode) {
              // expecting object array on stack
              GeneratorAdapter ga =
                  new GeneratorAdapter(mv, source.access, source.name, source.desc);
              Type[] argumentTypes = ga.getArgumentTypes();
              for (int i = 0; i < writableArguments.size(); i++) {
                OutputArgument argument = writableArguments.get(i);
                ga.dup();
                ga.push(returnIndex + i);
                ga.loadArg(argument.adviceIndex);
                ga.box(argumentTypes[argument.adviceIndex]);
                ga.arrayStore(OBJECT_TYPE);
              }
            }
            super.visitInsn(opcode);
          }
        };
    result = makeReadOnly(Advice.Argument.class, result);
    return result;
  }

  /**
   * Transform arguments annotated with {@code @Advice.Return(readOnly = false)}.
   *
   * <pre>{@code
   * void foo(@Advice.Return(readOnly = false) T1 foo)
   * }</pre>
   *
   * <p>is transformed to
   *
   * <pre>{@code
   * @Advice.AssignReturned.ToReturned(index = 0, typing = DYNAMIC)
   * Object[] foo(@Advice.Return(readOnly = true) T foo) {
   *   ...
   *   return new Object[] { foo };
   * }
   * }</pre>
   */
  private static MethodVisitor instrumentWritableReturn(
      MethodVisitor target, MethodNode source, OutputArgument writableReturn, int returnIndex) {
    MethodVisitor result =
        new MethodVisitor(AsmApi.VERSION, target) {
          @Override
          public void visitCode() {
            AnnotationVisitor av =
                visitAnnotation(Type.getDescriptor(Advice.AssignReturned.ToReturned.class), true);
            av.visit("index", returnIndex);
            av.visitEnum("typing", Type.getDescriptor(Assigner.Typing.class), "DYNAMIC");
            av.visitEnd();
            super.visitCode();
          }

          @Override
          public void visitInsn(int opcode) {
            if (Opcodes.ARETURN == opcode) {
              // expecting object array on stack
              GeneratorAdapter ga =
                  new GeneratorAdapter(mv, source.access, source.name, source.desc);
              Type[] argumentTypes = ga.getArgumentTypes();
              ga.dup();
              ga.push(returnIndex);
              ga.loadArg(writableReturn.adviceIndex);
              ga.box(argumentTypes[writableReturn.adviceIndex]);
              ga.arrayStore(OBJECT_TYPE);
            }
            super.visitInsn(opcode);
          }
        };
    result = makeReadOnly(Advice.Return.class, result);
    return result;
  }

  /**
   * Transform arguments annotated with {@code @Advice.Local} and {@code @Advice.Enter}.
   *
   * <pre>{@code
   * void foo(@Advice.Local("foo") T1 foo, @Advice.Local("bar") T2 bar)
   * }</pre>
   *
   * <p>for enter advice is transformed to
   *
   * <pre>{@code
   * Object[] foo(@Advice.Unused Object foo, @Advice.Unused Object bar) {
   *   ...
   *   Map result = new HashMap();
   *   result.put("foo", foo);
   *   result.put("bar", bar);
   *   return new Object[] { result };
   * }
   * }</pre>
   *
   * <p>and for exit advice is transformed to
   *
   * <pre>{@code
   * void foo(@Advice.Unused Object foo, @Advice.Unused Object bar, @Advice.Enter Object[] array) {
   *   Map map = (Map) array[0];
   *   foo = (T1) map.get("foo");
   *   bar = (T2) map.get("bar");
   *   ...
   * }
   * }</pre>
   */
  private static MethodVisitor instrumentAdviceLocals(
      boolean isEnterAdvice,
      MethodVisitor target,
      MethodNode source,
      String originalDesc,
      List<AdviceLocal> adviceLocals,
      OutputArgument enterArgument,
      int returnIndex) {
    AtomicReference<GeneratorAdapter> generatorRef = new AtomicReference<>();
    AtomicInteger dataIndex = new AtomicInteger();

    target =
        new MethodVisitor(AsmApi.VERSION, target) {
          @Override
          public AnnotationVisitor visitParameterAnnotation(
              int parameter, String descriptor, boolean visible) {
            // replace @Advice.Local with @Advice.Unused
            if (Type.getDescriptor(Advice.Local.class).equals(descriptor)) {
              descriptor = Type.getDescriptor(Advice.Unused.class);
            }
            // replace @Advice.Enter with @Advice.Unused
            if (enterArgument != null
                && enterArgument.adviceIndex == parameter
                && Type.getDescriptor(Advice.Enter.class).equals(descriptor)) {
              descriptor = Type.getDescriptor(Advice.Unused.class);
            }
            return super.visitParameterAnnotation(parameter, descriptor, visible);
          }

          @Override
          public void visitAnnotableParameterCount(int parameterCount, boolean visible) {
            if (!isEnterAdvice) {
              parameterCount++;
            }
            super.visitAnnotableParameterCount(parameterCount, visible);
          }

          @Override
          public void visitInsn(int opcode) {
            if (isEnterAdvice && Opcodes.ARETURN == opcode) {
              // expecting object array on stack
              GeneratorAdapter ga = generatorRef.get();
              // duplicate array
              ga.dup();
              // push array index for the map
              ga.push(returnIndex);
              Type hashMapType = Type.getType(HashMap.class);
              Type[] argumentTypes = ga.getArgumentTypes();
              ga.newInstance(hashMapType);
              ga.dup();
              ga.invokeConstructor(hashMapType, Method.getMethod("void <init>()"));
              // stack: array, array, array index for map, map
              for (AdviceLocal adviceLocal : adviceLocals) {
                ga.dup();
                ga.push(adviceLocal.name);
                ga.loadArg(adviceLocal.adviceIndex);
                ga.box(argumentTypes[adviceLocal.adviceIndex]);
                ga.invokeVirtual(
                    hashMapType,
                    Method.getMethod("java.lang.Object put(java.lang.Object, java.lang.Object)"));
                // pop return value of Map.put
                ga.pop();
              }
              // stack: array, array, array index for map, map
              // store map in the array
              ga.arrayStore(OBJECT_TYPE);
              // stack: array
            }
            super.visitInsn(opcode);
          }

          @Override
          public void visitCode() {
            super.visitCode();
            GeneratorAdapter ga = generatorRef.get();
            Type[] argumentTypes = ga.getArgumentTypes();

            if (isEnterAdvice) {
              // we have changed the type fo method arguments annotated with @Advice.Local to Object
              // here we'll load the argument, cast it to its actual type, and store it back
              for (AdviceLocal adviceLocal : adviceLocals) {
                ga.loadArg(adviceLocal.adviceIndex);
                ga.checkCast(argumentTypes[adviceLocal.adviceIndex]);
                ga.storeArg(adviceLocal.adviceIndex);
              }
              return;
            }

            // the index of last argument where object array returned from enter advice is inserted
            // (argumentTypes array does not contain the object array)
            int lastArgumentIndex = argumentTypes.length;
            AnnotationVisitor av =
                mv.visitParameterAnnotation(
                    lastArgumentIndex, Type.getDescriptor(Advice.Enter.class), true);
            av.visitEnd();

            // load object array
            ga.loadLocal(dataIndex.get(), OBJECT_ARRAY_TYPE);
            if (enterArgument != null) {
              // value for @Advice.Enter is stored as the first element
              ga.dup();
              ga.push(0);
              Type type = argumentTypes[enterArgument.adviceIndex];
              ga.arrayLoad(type);
              ga.checkCast(type);
              ga.storeArg(enterArgument.adviceIndex);
            }

            // object array on stack
            ga.dup();
            Type mapType = Type.getType(Map.class);
            // we want the last element of the array
            ga.arrayLength();
            ga.visitInsn(Opcodes.ICONST_1);
            ga.visitInsn(Opcodes.ISUB);
            // load map
            ga.arrayLoad(mapType);
            for (AdviceLocal adviceLocal : adviceLocals) {
              // duplicate Map
              ga.dup();
              ga.push(adviceLocal.name);
              ga.invokeInterface(
                  mapType, Method.getMethod("java.lang.Object get(java.lang.Object)"));
              ga.unbox(argumentTypes[adviceLocal.adviceIndex]);
              ga.storeArg(adviceLocal.adviceIndex);
            }
            // pop Map
            ga.pop();
          }
        };

    // pretend that this method still takes the original arguments
    GeneratorAdapter ga =
        new GeneratorAdapter(target, Opcodes.ACC_STATIC, source.name, originalDesc);
    generatorRef.set(ga);
    if (!isEnterAdvice) {
      // for exit advice create a new local for the Object array we added as the last method
      // argument
      dataIndex.set(ga.newLocal(OBJECT_ARRAY_TYPE));
    }

    return ga;
  }

  private static void instrument(
      TransformationContext context, MethodNode methodNode, ClassVisitor classVisitor) {
    String originalDescriptor = methodNode.desc;
    String[] exceptionsArray = methodNode.exceptions.toArray(new String[0]);

    List<OutputArgument> writableArguments = getWritableArguments(methodNode);
    OutputArgument writableReturn = getWritableReturnValue(methodNode);
    OutputArgument enterArgument = getEnterArgument(methodNode);
    List<AdviceLocal> adviceLocals = getLocals(methodNode);
    boolean isEnterAdvice = isEnterAdvice(methodNode);
    boolean isExitAdvice = isExitAdvice(methodNode);
    Type returnType = Type.getReturnType(methodNode.desc);

    // currently we don't support rewriting enter advice returning a primitive type
    if (isEnterAdvice
        && !(returnType.getSort() == Type.VOID
            || returnType.getSort() == Type.OBJECT
            || returnType.getSort() == Type.ARRAY)) {
      context.disableReturnTypeChange();
    }
    // context is shared by enter and exit advice, if entry advice was rejected don't attempt to
    // rewrite usages of @Advice.Enter in the exit advice
    if (!context.canChangeReturnType()) {
      enterArgument = null;
    }

    if (context.canChangeReturnType() || (isExitAdvice && Type.VOID_TYPE.equals(returnType))) {
      if (!writableArguments.isEmpty()
          || writableReturn != null
          || !Type.VOID_TYPE.equals(returnType)
          || (!adviceLocals.isEmpty() && isEnterAdvice)) {
        Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc);
        if (!adviceLocals.isEmpty() && isEnterAdvice) {
          // Set type of arguments annotated with @Advice.Local to Object. These arguments are
          // likely to be helper classes which currently breaks because the invokedynamic call in
          // advised class needs access to the parameter types of the advice method.
          for (AdviceLocal adviceLocal : adviceLocals) {
            argumentTypes[adviceLocal.adviceIndex] = OBJECT_TYPE;
          }
        }

        methodNode.desc = Type.getMethodDescriptor(OBJECT_ARRAY_TYPE, argumentTypes);

        MethodNode tmp =
            new MethodNode(
                methodNode.access,
                methodNode.name,
                methodNode.desc,
                methodNode.signature,
                exceptionsArray);
        MethodVisitor mv =
            instrumentOurParameters(
                context,
                tmp,
                methodNode,
                originalDescriptor,
                writableArguments,
                writableReturn,
                adviceLocals);
        methodNode.accept(mv);

        methodNode = tmp;
        adviceLocals = getLocals(methodNode);
      }

      // this is the only transformation that does not change the return type of the advice method,
      // thus it is also the only transformation that can be applied on top of the other transforms
      if ((!adviceLocals.isEmpty() || enterArgument != null) && isExitAdvice) {
        // Set type of arguments annotated with @Advice.Local to Object. These arguments are likely
        // to be helper classes which currently breaks because the invokedynamic call in advised
        // class needs access to the parameter types of the advice method.
        Type[] newArgumentTypes = Type.getArgumentTypes(methodNode.desc);
        for (AdviceLocal adviceLocal : adviceLocals) {
          newArgumentTypes[adviceLocal.adviceIndex] = OBJECT_TYPE;
        }
        if (enterArgument != null) {
          newArgumentTypes[enterArgument.adviceIndex] = OBJECT_TYPE;
        }
        List<Type> typeList = new ArrayList<>(Arrays.asList(newArgumentTypes));
        // add Object array as the last argument, this array is used to pass info from the enter
        // advice
        typeList.add(OBJECT_ARRAY_TYPE);

        methodNode.desc =
            Type.getMethodDescriptor(
                Type.getReturnType(methodNode.desc), typeList.toArray(new Type[0]));

        MethodNode tmp =
            new MethodNode(
                methodNode.access,
                methodNode.name,
                methodNode.desc,
                methodNode.signature,
                exceptionsArray);
        MethodVisitor mv =
            instrumentAdviceLocals(
                false, tmp, methodNode, originalDescriptor, adviceLocals, enterArgument, -1);
        methodNode.accept(mv);

        methodNode = tmp;
      }
    }

    MethodVisitor mv =
        classVisitor.visitMethod(
            methodNode.access,
            methodNode.name,
            methodNode.desc,
            methodNode.signature,
            exceptionsArray);
    mv = delegateAdvice(context, mv);

    methodNode.accept(mv);
  }

  private static MethodVisitor instrumentOurParameters(
      TransformationContext context,
      MethodVisitor target,
      MethodNode source,
      String originalDesc,
      List<OutputArgument> writableArguments,
      OutputArgument writableReturn,
      List<AdviceLocal> adviceLocals) {

    // position 0 in enter advice is reserved for the return value of the method
    // to avoid complicating things further we aren't going to figure out whether it is really used
    int returnArraySize = isEnterAdvice(source) ? 1 : 0;
    if (writableReturn != null) {
      target = instrumentWritableReturn(target, source, writableReturn, returnArraySize);
      returnArraySize++;
    }
    if (!writableArguments.isEmpty()) {
      target = instrumentWritableArguments(target, source, writableArguments, returnArraySize);
      returnArraySize += writableArguments.size();
    }
    if (!adviceLocals.isEmpty() && isEnterAdvice(source)) {
      target =
          instrumentAdviceLocals(
              true, target, source, originalDesc, adviceLocals, null, returnArraySize);
      returnArraySize++;
    }
    target = addReturnArray(context, target, returnArraySize);

    return target;
  }

  /** Return the value of the {@code readOnly} attribute of the annotation. */
  private static boolean isWriteable(AnnotationNode annotationNode) {
    Object value = getAttributeValue(annotationNode, "readOnly");
    return Boolean.FALSE.equals(value);
  }

  private static Object getAttributeValue(AnnotationNode annotationNode, String attributeName) {
    if (annotationNode.values != null && !annotationNode.values.isEmpty()) {
      List<Object> values = annotationNode.values;
      for (int i = 0; i < values.size(); i += 2) {
        String name = (String) values.get(i);
        Object value = values.get(i + 1);
        if (attributeName.equals(name)) {
          return value;
        }
      }
    }

    return null;
  }

  /** Return the value of the {@code value} attribute of the annotation. */
  private static Object getAnnotationValue(AnnotationNode annotationNode) {
    if (annotationNode.values != null && !annotationNode.values.isEmpty()) {
      List<Object> values = annotationNode.values;
      for (int i = 0; i < values.size(); i += 2) {
        String attributeName = (String) values.get(i);
        Object attributeValue = values.get(i + 1);
        if ("value".equals(attributeName)) {
          return attributeValue;
        }
      }
    }

    return null;
  }

  private static MethodVisitor addReturnArray(
      TransformationContext context, MethodVisitor target, int returnArraySize) {
    return new MethodVisitor(AsmApi.VERSION, target) {
      @Override
      public void visitInsn(int opcode) {
        if (Opcodes.RETURN == opcode) {
          // change the return value of the method to Object[]
          GeneratorAdapter ga = new GeneratorAdapter(mv, 0, null, "()V");
          ga.push(returnArraySize);
          ga.newArray(OBJECT_TYPE);
          opcode = Opcodes.ARETURN;
        } else if (context.canChangeReturnType() && Opcodes.ARETURN == opcode) {
          // change the return value of the method to Object[] that on the 0 index contains the
          // original return value

          // stack: original return value
          GeneratorAdapter ga = new GeneratorAdapter(mv, 0, null, "()V");
          ga.push(returnArraySize);
          ga.newArray(OBJECT_TYPE);
          // stack: original return value, array
          ga.dupX1();
          // stack: array, original return value, array
          ga.swap();
          // stack: array, array, original return value
          ga.push(0); // original return value is stored as the first element
          ga.swap();
          ga.arrayStore(OBJECT_TYPE);
          // stack: array
        }
        super.visitInsn(opcode);
      }
    };
  }

  /**
   * If method is annotated with {@link Advice.OnMethodEnter} or {@link Advice.OnMethodExit} set
   * {@code inline} attribute on the annotation to {@code false}. If method is annotated with {@link
   * Advice.OnMethodEnter} and has {@code skipOn} attribute set {@code skipOnIndex} attribute on the
   * annotation to {@code 0}.
   */
  private static MethodVisitor delegateAdvice(TransformationContext context, MethodVisitor target) {
    return new MethodVisitor(AsmApi.VERSION, target) {
      @Override
      public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        AnnotationVisitor av = super.visitAnnotation(descriptor, visible);
        Type type = Type.getType(descriptor);
        if (!Type.getType(Advice.OnMethodEnter.class).equals(type)
            && !Type.getType(Advice.OnMethodExit.class).equals(type)) {
          return av;
        }
        return new AnnotationVisitor(api, av) {
          boolean hasInline = false;
          boolean hasSkipOn = false;

          @Override
          public void visit(String name, Object value) {
            if ("inline".equals(name)) {
              value = Boolean.FALSE;
              hasInline = true;
            } else if ("skipOn".equals(name) && value != void.class) {
              hasSkipOn = true;
            }
            super.visit(name, value);
          }

          @Override
          public void visitEnd() {
            if (!hasInline) {
              visit("inline", Boolean.FALSE);
            }
            if (context.canChangeReturnType() && hasSkipOn) {
              visit("skipOnIndex", 0);
            }
            super.visitEnd();
          }
        };
      }
    };
  }

  /** If annotation has {@code readOnly} attribute set it to {@code true}. */
  private static MethodVisitor makeReadOnly(Class<?> annotationType, MethodVisitor target) {
    return new MethodVisitor(AsmApi.VERSION, target) {

      @Override
      public AnnotationVisitor visitParameterAnnotation(
          int parameter, String descriptor, boolean visible) {
        AnnotationVisitor av = super.visitParameterAnnotation(parameter, descriptor, visible);
        Type type = Type.getType(descriptor);
        if (!Type.getType(annotationType).equals(type)) {
          return av;
        }
        return new AnnotationVisitor(api, av) {
          @Override
          public void visit(String name, Object value) {
            if ("readOnly".equals(name)) {
              value = Boolean.TRUE;
            }
            super.visit(name, value);
          }
        };
      }
    };
  }

  private static class TransformationContext {
    private boolean canChangeReturnType = true;

    void disableReturnTypeChange() {
      canChangeReturnType = false;
    }

    boolean canChangeReturnType() {
      return canChangeReturnType;
    }
  }

  private AdviceTransformer() {}
}
