/*
 *                                Apache License
 *                          Version 2.0, January 2004
 *                       http://www.apache.org/licenses/
 *
 *  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 *
 *  1. Definitions.
 *
 *     "License" shall mean the terms and conditions for use, reproduction,
 *     and distribution as defined by Sections 1 through 9 of this document.
 *
 *     "Licensor" shall mean the copyright owner or entity authorized by
 *     the copyright owner that is granting the License.
 *
 *     "Legal Entity" shall mean the union of the acting entity and all
 *     other entities that control, are controlled by, or are under common
 *     control with that entity. For the purposes of this definition,
 *     "control" means (i) the power, direct or indirect, to cause the
 *     direction or management of such entity, whether by contract or
 *     otherwise, or (ii) ownership of fifty percent (50%) or more of the
 *     outstanding shares, or (iii) beneficial ownership of such entity.
 *
 *     "You" (or "Your") shall mean an individual or Legal Entity
 *     exercising permissions granted by this License.
 *
 *     "Source" form shall mean the preferred form for making modifications,
 *     including but not limited to software source code, documentation
 *     source, and configuration files.
 *
 *     "Object" form shall mean any form resulting from mechanical
 *     transformation or translation of a Source form, including but
 *     not limited to compiled object code, generated documentation,
 *     and conversions to other media types.
 *
 *     "Work" shall mean the work of authorship, whether in Source or
 *     Object form, made available under the License, as indicated by a
 *     copyright notice that is included in or attached to the work
 *     (an example is provided in the Appendix below).
 *
 *     "Derivative Works" shall mean any work, whether in Source or Object
 *     form, that is based on (or derived from) the Work and for which the
 *     editorial revisions, annotations, elaborations, or other modifications
 *     represent, as a whole, an original work of authorship. For the purposes
 *     of this License, Derivative Works shall not include works that remain
 *     separable from, or merely link (or bind by name) to the interfaces of,
 *     the Work and Derivative Works thereof.
 *
 *     "Contribution" shall mean any work of authorship, including
 *     the original version of the Work and any modifications or additions
 *     to that Work or Derivative Works thereof, that is intentionally
 *     submitted to Licensor for inclusion in the Work by the copyright owner
 *     or by an individual or Legal Entity authorized to submit on behalf of
 *     the copyright owner. For the purposes of this definition, "submitted"
 *     means any form of electronic, verbal, or written communication sent
 *     to the Licensor or its representatives, including but not limited to
 *     communication on electronic mailing lists, source code control systems,
 *     and issue tracking systems that are managed by, or on behalf of, the
 *     Licensor for the purpose of discussing and improving the Work, but
 *     excluding communication that is conspicuously marked or otherwise
 *     designated in writing by the copyright owner as "Not a Contribution."
 *
 *     "Contributor" shall mean Licensor and any individual or Legal Entity
 *     on behalf of whom a Contribution has been received by Licensor and
 *     subsequently incorporated within the Work.
 *
 *  2. Grant of Copyright License. Subject to the terms and conditions of
 *     this License, each Contributor hereby grants to You a perpetual,
 *     worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 *     copyright license to reproduce, prepare Derivative Works of,
 *     publicly display, publicly perform, sublicense, and distribute the
 *     Work and such Derivative Works in Source or Object form.
 *
 *  3. Grant of Patent License. Subject to the terms and conditions of
 *     this License, each Contributor hereby grants to You a perpetual,
 *     worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 *     (except as stated in this section) patent license to make, have made,
 *     use, offer to sell, sell, import, and otherwise transfer the Work,
 *     where such license applies only to those patent claims licensable
 *     by such Contributor that are necessarily infringed by their
 *     Contribution(s) alone or by combination of their Contribution(s)
 *     with the Work to which such Contribution(s) was submitted. If You
 *     institute patent litigation against any entity (including a
 *     cross-claim or counterclaim in a lawsuit) alleging that the Work
 *     or a Contribution incorporated within the Work constitutes direct
 *     or contributory patent infringement, then any patent licenses
 *     granted to You under this License for that Work shall terminate
 *     as of the date such litigation is filed.
 *
 *  4. Redistribution. You may reproduce and distribute copies of the
 *     Work or Derivative Works thereof in any medium, with or without
 *     modifications, and in Source or Object form, provided that You
 *     meet the following conditions:
 *
 *     (a) You must give any other recipients of the Work or
 *         Derivative Works a copy of this License and
 *
 *     (b) You must cause any modified files to carry prominent notices
 *         stating that You changed the files and
 *
 *     (c) You must retain, in the Source form of any Derivative Works
 *         that You distribute, all copyright, patent, trademark, and
 *         attribution notices from the Source form of the Work,
 *         excluding those notices that do not pertain to any part of
 *         the Derivative Works and
 *
 *     (d) If the Work includes a "NOTICE" text file as part of its
 *         distribution, then any Derivative Works that You distribute must
 *         include a readable copy of the attribution notices contained
 *         within such NOTICE file, excluding those notices that do not
 *         pertain to any part of the Derivative Works, in at least one
 *         of the following places: within a NOTICE text file distributed
 *         as part of the Derivative Works within the Source form or
 *         documentation, if provided along with the Derivative Works or,
 *         within a display generated by the Derivative Works, if and
 *         wherever such third-party notices normally appear. The contents
 *         of the NOTICE file are for informational purposes only and
 *         do not modify the License. You may add Your own attribution
 *         notices within Derivative Works that You distribute, alongside
 *         or as an addendum to the NOTICE text from the Work, provided
 *         that such additional attribution notices cannot be construed
 *         as modifying the License.
 *
 *     You may add Your own copyright statement to Your modifications and
 *     may provide additional or different license terms and conditions
 *     for use, reproduction, or distribution of Your modifications, or
 *     for any such Derivative Works as a whole, provided Your use,
 *     reproduction, and distribution of the Work otherwise complies with
 *     the conditions stated in this License.
 *
 *  5. Submission of Contributions. Unless You explicitly state otherwise,
 *     any Contribution intentionally submitted for inclusion in the Work
 *     by You to the Licensor shall be under the terms and conditions of
 *     this License, without any additional terms or conditions.
 *     Notwithstanding the above, nothing herein shall supersede or modify
 *     the terms of any separate license agreement you may have executed
 *     with Licensor regarding such Contributions.
 *
 *  6. Trademarks. This License does not grant permission to use the trade
 *     names, trademarks, service marks, or product names of the Licensor,
 *     except as required for reasonable and customary use in describing the
 *     origin of the Work and reproducing the content of the NOTICE file.
 *
 *  7. Disclaimer of Warranty. Unless required by applicable law or
 *     agreed to in writing, Licensor provides the Work (and each
 *     Contributor provides its Contributions) on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 *     implied, including, without limitation, any warranties or conditions
 *     of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 *     PARTICULAR PURPOSE. You are solely responsible for determining the
 *     appropriateness of using or redistributing the Work and assume any
 *     risks associated with Your exercise of permissions under this License.
 *
 *  8. Limitation of Liability. In no event and under no legal theory,
 *     whether in tort (including negligence), contract, or otherwise,
 *     unless required by applicable law (such as deliberate and grossly
 *     negligent acts) or agreed to in writing, shall any Contributor be
 *     liable to You for damages, including any direct, indirect, special,
 *     incidental, or consequential damages of any character arising as a
 *     result of this License or out of the use or inability to use the
 *     Work (including but not limited to damages for loss of goodwill,
 *     work stoppage, computer failure or malfunction, or any and all
 *     other commercial damages or losses), even if such Contributor
 *     has been advised of the possibility of such damages.
 *
 *  9. Accepting Warranty or Additional Liability. While redistributing
 *     the Work or Derivative Works thereof, You may choose to offer,
 *     and charge a fee for, acceptance of support, warranty, indemnity,
 *     or other liability obligations and/or rights consistent with this
 *     License. However, in accepting such obligations, You may act only
 *     on Your own behalf and on Your sole responsibility, not on behalf
 *     of any other Contributor, and only if You agree to indemnify,
 *     defend, and hold each Contributor harmless for any liability
 *     incurred by, or claims asserted against, such Contributor by reason
 *     of your accepting any such warranty or additional liability.
 *
 *  END OF TERMS AND CONDITIONS
 *
 *  APPENDIX: How to apply the Apache License to your work.
 *
 *     To apply the Apache License to your work, attach the following
 *     boilerplate notice, with the fields enclosed by brackets "[]"
 *     replaced with your own identifying information. (Don't include
 *     the brackets!)  The text should be enclosed in the appropriate
 *     comment syntax for the file format. We also recommend that a
 *     file or class name and description of purpose be included on the
 *     same "printed page" as the copyright notice for easier
 *     identification within third-party archives.
 *
 *  Copyright 2018 pengfengwang
 *
 *  Licensed under the Apache License, Version 2.0 (the "License")
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package com.ximsfei.stark.gradle.asm.monitor;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.collect.ImmutableList;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
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.JSRInlinerAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodNode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static com.google.common.base.Preconditions.checkNotNull;

public class RedirectionVisitor extends MonitorVisitor {

    private boolean disableRedirectionForClass = false;
    private boolean isInterface = false;
    private boolean classInitializerAdded = false;

    private static final class VisitorBuilder implements MonitorVisitor.VisitorBuilder {

        @Override
        public MonitorVisitor build(@NonNull AsmClassNode classNode, @NonNull ClassVisitor classVisitor) {
            return new RedirectionVisitor(classNode, classVisitor);
        }

        @Override
        public String getMangledRelativeClassFilePath(@NonNull String originalClassFilePath) {
            return originalClassFilePath;
        }
    }

    @NonNull
    public static final MonitorVisitor.VisitorBuilder VISITOR_BUILDER = new VisitorBuilder();

    public RedirectionVisitor(
            @NonNull AsmClassNode classAndInterfaceNode, @NonNull ClassVisitor classVisitor) {
        super(classAndInterfaceNode, classVisitor);
    }


    /**
     * Ensures that the class contains a $starkChange field used for referencing the IncrementalChange
     * dispatcher.
     * <p>
     * <p>Also updates package_private visibility to public so we can call into this class from
     * outside the package.
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName,
                      String[] interfaces) {
        visitedClassName = name;
        visitedSuperName = superName;
        isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
        int fieldAccess =
                isInterface
                        ? Opcodes.ACC_PUBLIC
                        | Opcodes.ACC_STATIC
                        | Opcodes.ACC_SYNTHETIC
                        | Opcodes.ACC_FINAL
                        : Opcodes.ACC_PUBLIC
                        | Opcodes.ACC_STATIC
                        | Opcodes.ACC_VOLATILE
                        | Opcodes.ACC_SYNTHETIC
                        | Opcodes.ACC_TRANSIENT;
        // when dealing with interfaces, the $starkChange field is an AtomicReference to the CHANGE_TYPE
        // since fields in interface must be final. For classes, it's the CHANGE_TYPE directly.
        if (isInterface) {
            super.visitField(
                    fieldAccess,
                    "$starkChange",
                    getRuntimeTypeName(Type.getType(AtomicReference.class)),
                    null,
                    null);
        } else {
            super.visitField(fieldAccess, "$starkChange", getRuntimeTypeName(CHANGE_TYPE), null, null);
        }
        access = transformClassAccessForStark(access);
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        int newAccess =
                access & (~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) | Opcodes.ACC_PUBLIC;
        super.visitInnerClass(name, outerName, innerName, newAccess);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
//        if (desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) {
//            disableRedirectionForClass = true;
//        }
        return super.visitAnnotation(desc, visible);
    }


    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature,
                                   Object value) {

        access = transformAccessForStark(access);
        return super.visitField(access, name, desc, signature, value);
    }

    /**
     * Insert Constructor specific logic({@link ConstructorRedirection} and
     * {@link ConstructorBuilder}) for constructor redirecting or
     * normal method redirecting ({@link MethodRedirection}) for other methods.
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                     String[] exceptions) {

        access = transformAccessForStark(access);

        MethodVisitor defaultVisitor = super.visitMethod(access, name, desc, signature, exceptions);
        MethodNode method =
                checkNotNull(
                        getMethodByNameInClass(name, desc, classAndInterfaceNode),
                        "Method found by visitor but not in the pre-parsed class node.");

        // does the method use blacklisted APIs.
        boolean hasIncompatibleChange = StarkMethodVerifier.verifyMethod(method)
                != StarkVerifierStatus.COMPATIBLE;

        if (hasIncompatibleChange
                || disableRedirectionForClass
                || !isAccessCompatibleWithStark(access)) {
            return defaultVisitor;
        }
        if (name.equals(ByteCodeUtils.CLASS_INITIALIZER)) {
            classInitializerAdded = true;
            return isInterface
                    ? new ISInterfaceStaticInitializerMethodVisitor(
                    defaultVisitor, access, name, desc)
                    : defaultVisitor;
        }

        ArrayList<Type> args = new ArrayList<>(Arrays.asList(Type.getArgumentTypes(desc)));
        boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
        if (!isStatic) {
            args.add(0, Type.getType(Object.class));
        }

        // Install the Jsr/Ret inliner adapter, we have had reports of code still using the
        // Jsr/Ret deprecated byte codes.
        // see https://code.google.com/p/android/issues/detail?id=220019
        JSRInlinerAdapter jsrInlinerAdapter =
                new JSRInlinerAdapter(defaultVisitor, access, name, desc, signature, exceptions);

        ISAbstractMethodVisitor mv =
                isInterface
                        ? new ISDefaultMethodVisitor(jsrInlinerAdapter, access, name, desc)
                        : new ISMethodVisitor(jsrInlinerAdapter, access, name, desc);

        if (name.equals(ByteCodeUtils.CONSTRUCTOR)) {
            if ((access & Opcodes.ACC_SYNTHETIC) != 0
                    || ByteCodeUtils.isAnnotatedWith(method, "Lkotlin/jvm/JvmOverloads;")) {
                return defaultVisitor;
            }
            Constructor constructor = ConstructorBuilder.build(visitedClassName, method);
            LabelNode start = new LabelNode();
            method.instructions.insert(constructor.loadThis, start);
            if (constructor.lineForLoad != -1) {
                // Record the line number from the start of LOAD_0 for uninitialized 'this'.
                // This allows a breakpoint to be set at the line with this(...) or super(...)
                // call in the constructor.
                method.instructions.insert(
                        constructor.loadThis, new LineNumberNode(constructor.lineForLoad, start));
            }
            mv.addRedirection(new ConstructorRedirection(start, constructor, args));
        } else {
            mv.addRedirection(
                    new MethodRedirection(
                            new LabelNode(mv.getStartLabel()),
                            name + "." + desc,
                            args,
                            Type.getReturnType(desc)));
        }
        method.accept(mv);
        return null;
    }

    /**
     * If a class is package private, make it public so instrumented code living in a different
     * class loader can instantiate them.
     *
     * @param access the original class/method/field access.
     * @return the new access or the same one depending on the original access rights.
     */
    private static int transformClassAccessForStark(int access) {
        AccessRight accessRight = AccessRight.fromNodeAccess(access);

        return accessRight == AccessRight.PACKAGE_PRIVATE ? access | Opcodes.ACC_PUBLIC : access;
    }

    /**
     * If a method/field is not private, make it public. This is to workaround the fact
     * <p>
     * <ul>
     * reload.dex are loaded from a different class loader but private methods/fields are accessed
     * through reflection, therefore you need visibility.
     * </ul>
     * <p>
     * remember that in Java, protected methods or fields can be accessed by classes in the same
     * package : {@see https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html}
     *
     * @param access the original class/method/field access.
     * @return the new access or the same one depending on the original access rights.
     */
    private static int transformAccessForStark(int access) {
        AccessRight accessRight = AccessRight.fromNodeAccess(access);
        if (accessRight != AccessRight.PRIVATE) {
            access &= ~Opcodes.ACC_PROTECTED;
            access &= ~Opcodes.ACC_PRIVATE;
            return access | Opcodes.ACC_PUBLIC;
        }
        return access;
    }

    private abstract static class ISAbstractMethodVisitor extends GeneratorAdapter {

        protected boolean disableRedirection = false;
        protected int change;
        protected final List<Type> args;
        protected final List<Redirection> redirections;
        protected final Map<Label, Redirection> resolvedRedirections;
        protected final Label start;

        public ISAbstractMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
            super(Opcodes.ASM5, mv, access, name, desc);
            this.change = -1;
            this.redirections = new ArrayList<>();
            this.resolvedRedirections = new HashMap<>();
            this.args = new ArrayList<>(Arrays.asList(Type.getArgumentTypes(desc)));
            this.start = new Label();
            boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
            // if this is not a static, we add a fictional first parameter what will contain the
            // "this" reference which can be loaded with ILOAD_0 bytecode.
            if (!isStatic) {
                args.add(0, Type.getType(Object.class));
            }
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
//            if (desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) {
//                disableRedirection = true;
//            }
            return super.visitAnnotation(desc, visible);
        }

        /**
         * inserts a new local '$starkChange' in each method that contains a reference to the type's
         * IncrementalChange dispatcher, this is done to avoid threading issues.
         * <p>
         * Pseudo code:
         * <code>
         * $package/IncrementalChange $local1 = $className$.$starkChange;
         * </code>
         */
        @Override
        public void visitCode() {
            if (!disableRedirection) {
                // Labels cannot be used directly as they are volatile between different visits,
                // so we must use LabelNode and resolve before visiting for better performance.
                for (Redirection redirection : redirections) {
                    resolvedRedirections.put(redirection.getPosition().getLabel(), redirection);
                }

                super.visitLabel(start);
                change = newLocal(CHANGE_TYPE);
                visitChangeField();
                storeLocal(change);

                redirectAt(start);
            }
            super.visitCode();
        }

        protected abstract void visitChangeField();

        @Override
        public void visitLabel(Label label) {
            super.visitLabel(label);
            redirectAt(label);
        }

        protected void redirectAt(Label label) {
            if (disableRedirection) {
                return;
            }
            Redirection redirection = resolvedRedirections.get(label);
            if (redirection != null) {
                // A special line number to mark this area of code.
                super.visitLineNumber(0, label);
                redirection.redirect(this, change);
            }
        }

        public void addRedirection(@NonNull Redirection redirection) {
            redirections.add(redirection);
        }

        @Override
        public void visitLocalVariable(
                String name, String desc, String signature, Label start, Label end, int index) {
            // In dex format, the argument names are separated from the local variable names. It
            // seems to be needed to declare the local argument variables from the beginning of
            // the methods for dex to pick that up. By inserting code before the first label we
            // break that. In Java this is fine, and the debugger shows the right thing. However
            // if we don't readjust the local variables, we just don't see the arguments.
            if (!disableRedirection && index < args.size()) {
                start = this.start;
            }
            super.visitLocalVariable(name, desc, signature, start, end, index);
        }

        public Label getStartLabel() {
            return start;
        }
    }

    private class ISMethodVisitor extends ISAbstractMethodVisitor {

        public ISMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
            super(mv, access, name, desc);
        }

        @Override
        protected void visitChangeField() {
            visitFieldInsn(
                    Opcodes.GETSTATIC,
                    visitedClassName,
                    "$starkChange",
                    getRuntimeTypeName(CHANGE_TYPE));
        }
    }

    private class ISDefaultMethodVisitor extends ISAbstractMethodVisitor {

        public ISDefaultMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
            super(mv, access, name, desc);
        }

        @Override
        protected void visitChangeField() {
            visitFieldInsn(
                    Opcodes.GETSTATIC,
                    visitedClassName,
                    "$starkChange",
                    getRuntimeTypeName(Type.getType(AtomicReference.class)));
            mv.visitMethodInsn(
                    Opcodes.INVOKEVIRTUAL,
                    "java/util/concurrent/atomic/AtomicReference",
                    "get",
                    "()Ljava/lang/Object;",
                    false);
        }
    }

    private class ISInterfaceStaticInitializerMethodVisitor extends GeneratorAdapter {
        public ISInterfaceStaticInitializerMethodVisitor(
                MethodVisitor mv, int access, String name, String desc) {
            super(Opcodes.ASM5, mv, access, name, desc);
        }

        @Override
        public void visitCode() {
            addInterfaceClassInitializerCode(this);
            super.visitCode();
        }
    }

    /**
     * Decorated {@link MethodNode} that maintains a reference to the class declaring the method.
     */
    private static class MethodReference {
        final MethodNode method;
        final ClassNode owner;

        private MethodReference(MethodNode method, ClassNode owner) {
            this.method = method;
            this.owner = owner;
        }

        private String getDefautlDispatchName() {
            return getDefaultDispatchName(method);
        }

        private static String getDefaultDispatchName(MethodNode method) {
            return method.name + "." + method.desc;
        }
    }

    /***
     * Inserts a trampoline to this class so that the updated methods can make calls to super
     * class methods.
     * <p>
     * Pseudo code for this trampoline:
     * <code>
     *   Object access$super($classType instance, String name, object[] args) {
     *      switch(name) {
     *          case "firstMethod.(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;":
     *            return super~instance.firstMethod((String)arg[0], arg[1]);
     *          case "secondMethod.(Ljava/lang/String;I)V":
     *            return super~instance.firstMethod((String)arg[0], arg[1]);
     *
     *          default:
     *            StringBuilder $local1 = new StringBuilder();
     *            $local1.append("Method not found ");
     *            $local1.append(name);
     *            $local1.append(" in " $classType $super implementation");
     *            throw new $package/StarkReloadException($local1.toString());
     *      }
     * </code>
     */
    private void createAccessSuper() {
        int access = Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC
                | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_VARARGS;
        Method m = new Method("access$super", "(L" + visitedClassName
                + ";Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;");
        MethodVisitor visitor = super.visitMethod(access,
                m.getName(),
                m.getDescriptor(),
                null, null);

        final GeneratorAdapter mv = new GeneratorAdapter(access, m, visitor);

        // Gather all methods from itself and its superclasses to generate a giant access$super
        // implementation.
        // This will work fine as long as we don't support adding methods to a class.
        final Map<String, MethodReference> uniqueMethods = new HashMap<>();
        if (classAndInterfaceNode.hasParent()) {
            addAllNewMethods(
                    classAndInterfaceNode.getClassNode(),
                    classAndInterfaceNode.getParent(),
                    uniqueMethods);
        } else {
            // if we cannot determine the parents for this class, let's blindly add all the
            // method of the current class as a gateway to a possible parent version.
            addAllNewMethods(
                    classAndInterfaceNode.getClassNode(),
                    classAndInterfaceNode.getClassNode(),
                    uniqueMethods);
        }

        // and gather all default methods of all directly/inherited implemented interfaces.
        for (AsmInterfaceNode implementedInterface : classAndInterfaceNode.getInterfaces()) {
            addDefaultMethods(
                    classAndInterfaceNode.getClassNode(), implementedInterface, uniqueMethods);

            implementedInterface.onAll(
                    interfaceNode -> {
                        addAllNewMethods(
                                classAndInterfaceNode.getClassNode(), interfaceNode, uniqueMethods);
                        return null;
                    });
        }

        new StringSwitch() {
            @Override
            void visitString() {
                mv.visitVarInsn(Opcodes.ALOAD, 1);
            }

            @Override
            void visitCase(String methodName) {
                MethodReference methodRef = uniqueMethods.get(methodName);

                mv.visitVarInsn(Opcodes.ALOAD, 0);

                Type[] args = Type.getArgumentTypes(methodRef.method.desc);
                int argc = 0;
                for (Type t : args) {
                    mv.visitVarInsn(Opcodes.ALOAD, 2);
                    mv.push(argc);
                    mv.visitInsn(Opcodes.AALOAD);
                    ByteCodeUtils.unbox(mv, t);
                    argc++;
                }

//                if (TRACING_ENABLED) {
//                    trace(mv, "super selected ", methodRef.owner.name,
//                            methodRef.method.name, methodRef.method.desc);
//                }
                String parentName = findParentClassForMethod(methodRef);
//                logger.verbose(
//                        "Generating access$super for %1$s recev %2$s",
//                        methodRef.method.name,
//                        parentName);

                // Call super on the other object, yup this works cos we are on the right place to
                // call from.
                mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
                        parentName,
                        methodRef.method.name,
                        methodRef.method.desc, false);

                Type ret = Type.getReturnType(methodRef.method.desc);
                if (ret.getSort() == Type.VOID) {
                    mv.visitInsn(Opcodes.ACONST_NULL);
                } else {
                    mv.box(ret);
                }
                mv.visitInsn(Opcodes.ARETURN);
            }

            @Override
            void visitDefault() {
                writeMissingMessageWithHash(mv, visitedClassName);
            }
        }.visit(mv, uniqueMethods.keySet());

        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /***
     * Inserts a trampoline to this class so that the updated methods can make calls to
     * constructors.
     *
     * <p>
     * Pseudo code for this trampoline:
     * <code>
     *   ClassName(Object[] args, Marker unused) {
     *      String name = (String) args[0];
     *      if (name.equals(
     *          "java/lang/ClassName.(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;")) {
     *        this((String)arg[1], arg[2]);
     *        return
     *      }
     *      if (name.equals("SuperClassName.(Ljava/lang/String;I)V")) {
     *        super((String)arg[1], (int)arg[2]);
     *        return;
     *      }
     *      ...
     *      StringBuilder $local1 = new StringBuilder();
     *      $local1.append("Method not found ");
     *      $local1.append(name);
     *      $local1.append(" in " $classType $super implementation");
     *      throw new $package/StarkReloadException($local1.toString());
     *   }
     * </code>
     */
    private void createDispatchingThis() {
        // Gather all methods from itself and its superclasses to generate a giant constructor
        // implementation.
        // This will work fine as long as we don't support adding constructors to classes.
        final Map<String, MethodNode> uniqueMethods = new HashMap<>();

        addAllNewConstructors(
                uniqueMethods,
                classAndInterfaceNode.getClassNode(),
                true /*keepPrivateConstructors*/);
        classAndInterfaceNode.onParents(
                parentClassNode -> {
                    addAllNewConstructors(
                            uniqueMethods, parentClassNode, false /*keepPrivateConstructors*/);
                    return null;
                });

        int access = Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC;

        Method m = new Method(ByteCodeUtils.CONSTRUCTOR,
                ConstructorRedirection.DISPATCHING_THIS_SIGNATURE);
        MethodVisitor visitor = super.visitMethod(0, m.getName(), m.getDescriptor(), null, null);
        final GeneratorAdapter mv = new GeneratorAdapter(access, m, visitor);

        mv.visitCode();
        // Mark this code as redirection code
        Label label = new Label();
        mv.visitLineNumber(0, label);

        // Get and store the constructor canonical name.
        mv.visitVarInsn(Opcodes.ALOAD, 1);
        mv.push(1);
        mv.visitInsn(Opcodes.AALOAD);
        mv.unbox(Type.getType("Ljava/lang/String;"));
        final int constructorCanonicalName = mv.newLocal(Type.getType("Ljava/lang/String;"));
        mv.storeLocal(constructorCanonicalName);

        new StringSwitch() {

            @Override
            void visitString() {
                mv.loadLocal(constructorCanonicalName);
            }

            @Override
            void visitCase(String canonicalName) {
                MethodNode methodNode = uniqueMethods.get(canonicalName);
                String owner = canonicalName.split("\\.")[0];

                // Parse method arguments and
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                Type[] args = Type.getArgumentTypes(methodNode.desc);
                int argc = 1;
                for (Type t : args) {
                    mv.visitVarInsn(Opcodes.ALOAD, 1);
                    mv.push(argc + 1);
                    mv.visitInsn(Opcodes.AALOAD);
                    ByteCodeUtils.unbox(mv, t);
                    argc++;
                }

                mv.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, ByteCodeUtils.CONSTRUCTOR,
                        methodNode.desc, false);

                mv.visitInsn(Opcodes.RETURN);
            }

            @Override
            void visitDefault() {
                writeMissingMessageWithHash(mv, visitedClassName);
            }
        }.visit(mv, uniqueMethods.keySet());

        mv.visitMaxs(1, 3);
        mv.visitEnd();
    }

    /**
     * add a static initializer to java8 interfaces (this visitor will only be called when default
     * methods are present).
     */
    private void addInterfaceClassInitializer() {
        if (classInitializerAdded) {
            return;
        }
        MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
        mv.visitCode();
        addInterfaceClassInitializerCode(mv);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(3, 0);
        mv.visitEnd();
    }

    private void addInterfaceClassInitializerCode(MethodVisitor mv) {
        mv.visitTypeInsn(Opcodes.NEW, "java/util/concurrent/atomic/AtomicReference");
        mv.visitInsn(Opcodes.DUP);
        mv.visitInsn(Opcodes.ACONST_NULL);
        mv.visitMethodInsn(
                Opcodes.INVOKESPECIAL,
                "java/util/concurrent/atomic/AtomicReference",
                "<init>",
                "(Ljava/lang/Object;)V",
                false);
        mv.visitFieldInsn(
                Opcodes.PUTSTATIC,
                visitedClassName,
                "$starkChange",
                "Ljava/util/concurrent/atomic/AtomicReference;");
    }

    @Override
    public void visitEnd() {
        createAccessSuper();
        if (isInterface) {
            addInterfaceClassInitializer();
        } else {
            createDispatchingThis();
        }
        super.visitEnd();
    }

    /**
     * Find a suitable parent for a method reference. The method owner is not always a valid
     * parent to dispatch to. For instance, take the following example :
     * <code>
     * package a;
     * public class A {
     * public void publicMethod();
     * }
     * <p>
     * package a;
     * class B extends A {
     * public void publicMethod();
     * }
     * <p>
     * package b;
     * public class C extends B {
     * ...
     * }
     * </code>
     * when instrumenting C, the first method reference for "publicMethod" is on class B which we
     * cannot invoke directly since it's present on a private package B which is not located in the
     * same package as C. However C can still call the "publicMethod" since it's defined on A which
     * is a public class.
     * <p>
     * We cannot just blindly take the top most definition of "publicMethod" hoping this is the
     * accessible one since you can very well do :
     * <code>
     * package a;
     * class A {
     * public void publicMethod();
     * }
     * <p>
     * package a;
     * public class B extends A {
     * public void publicMethod();
     * }
     * <p>
     * package b;
     * public class C extends B {
     * ...
     * }
     * </code>
     * <p>
     * In that case, the top most parent class is the one defined the unaccessible method reference.
     * <p>
     * Therefore, the solution is to walk up the hierarchy until we find the same method defined on
     * an accessible class, if we cannot find such a method, the suitable parent is the parent class
     * of the visited class which is legal (but might consume a DEX id).
     *
     * @param methodReference the method reference to find a suitable parent for.
     * @return the parent class name
     */
    @NonNull
    String findParentClassForMethod(@NonNull MethodReference methodReference) {
//        logger.verbose(
//                "MethodRef %1$s access(%2$s) -> owner %3$s access(%4$s)",
//                methodReference.method.name, methodReference.method.access,
//                methodReference.owner.name, methodReference.owner.access);
        // if the method owner class is accessible from the visited class, just use that.
        if (isParentClassVisible(methodReference.owner, classAndInterfaceNode.getClassNode())) {
            if (!classAndInterfaceNode.isInterface(methodReference.owner)) {
                return methodReference.owner.name;
            }
        }
//        logger.verbose("Found an inaccessible methodReference %1$s", methodReference.method.name);

        // walk up the hierarchy, starting at the method reference owner.
        if (classAndInterfaceNode.hasParent()) {
            AsmClassNode asmClassNode = classAndInterfaceNode.getParent();
            while (!asmClassNode.getClassNode().name.equals(methodReference.owner.name)
                    && asmClassNode.hasParent()) {
                asmClassNode = asmClassNode.getParent();
            }
            if (asmClassNode.hasParent()) {
                String selectedParent =
                        asmClassNode.onParents(
                                parentClassNode -> {

                                    // check that this parent is visible, there might be several layers of package
                                    // private classes.
                                    if (isParentClassVisible(
                                            parentClassNode,
                                            classAndInterfaceNode.getClassNode())) {
                                        //noinspection unchecked: ASM API
                                        for (MethodNode methodNode :
                                                (List<MethodNode>) parentClassNode.methods) {
                                            // do not reference bridge methods, they might not be translated into dex, or
                                            // might disappear in the next javac compiler for that use case.
                                            if (methodNode.name.equals(methodReference.method.name)
                                                    && methodNode.desc.equals(
                                                    methodReference.method.desc)
                                                    && (methodNode.access
                                                    & (Opcodes.ACC_BRIDGE
                                                    | Opcodes.ACC_ABSTRACT))
                                                    == 0) {
//                                                logger.verbose(
//                                                        "Using class %1$s for dispatching %2$s:%3$s",
//                                                        parentClassNode.name,
//                                                        methodReference.method.name,
//                                                        methodReference.method.desc);
                                                return parentClassNode.name;
                                            }
                                        }
                                    }
                                    return null;
                                });
                if (selectedParent != null) {
                    return selectedParent;
                }
            }
        }
//        logger.verbose("Using immediate parent for dispatching %1$s", methodReference.method.desc);
        return classAndInterfaceNode.getClassNode().superName;
    }

    private static boolean isParentClassVisible(@NonNull ClassNode parent,
                                                @NonNull ClassNode child) {

        return ((parent.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0 ||
                (ByteCodeUtils.getPackageName(parent.name).equals(
                        ByteCodeUtils.getPackageName(child.name))));
    }

    /**
     * Add all unseen method from the passed ClassNode's methods and implemented interfaces.
     *
     * @param instrumentedClass class that is being visited
     * @param superClass        the class to save all directly implemented methods as well as default
     *                          methods from implemented interfaces from
     * @param methods           the methods already encountered in the ClassNode hierarchy
     * @see ClassNode#methods
     */
    private void addAllNewMethods(
            ClassNode instrumentedClass,
            AsmClassNode superClass,
            Map<String, MethodReference> methods) {

        superClass.onAll(
                classNode -> {
                    addAllNewMethods(instrumentedClass, classNode, methods);
                    return null;
                });
    }

    /**
     * Add all unseen methods from the passed ClassNode's methods.
     *
     * @param instrumentedClass class that is being visited
     * @param superClass        the class to save all new methods from
     * @param methods           the methods already encountered in the ClassNode hierarchy
     * @see ClassNode#methods
     */
    private List<MethodReference> addAllNewMethods(
            ClassNode instrumentedClass,
            ClassNode superClass,
            Map<String, MethodReference> methods) {

        ImmutableList.Builder<MethodReference> methodRefs = ImmutableList.builder();
        //noinspection unchecked
        for (MethodNode method : (List<MethodNode>) superClass.methods) {
            MethodReference methodRef =
                    addNewMethod(instrumentedClass, superClass, method, methods);
            if (methodRef != null) {
                methodRefs.add(methodRef);
            }
        }
        return methodRefs.build();
    }

    /**
     * Adds all default method for the passed implemented interface of a class as well as all
     * default methods from any interface extended by the passed implemented interface.
     *
     * @param instrumentedClass    the class we are bytecode instrumenting.
     * @param implementedInterface the interface that might contain default methods.
     * @param methods              map of methods we already encountered on the instrumentedClass implementation.
     * @return nothing
     */
    private Void addDefaultMethods(
            ClassNode instrumentedClass,
            AsmInterfaceNode implementedInterface,
            Map<String, MethodReference> methods) {

        Map<String, MethodReference> visitedInterfaceMethods = new HashMap<>();
        implementedInterface.onAll(
                interfaceNode -> {
                    addAllNewMethods(instrumentedClass, interfaceNode, methods)
                            .forEach(
                                    methodRef -> {
                                        visitedInterfaceMethods.put(
                                                methodRef.getDefautlDispatchName(), methodRef);
                                    });
                    return null;
                });

        // now make sure we have a gateway for inherited default methods that are not present
        // on the derived interface as calls can be made using the derived interface as the owner.
        for (MethodNode methodNode :
                (List<MethodNode>) implementedInterface.getClassNode().methods) {
            visitedInterfaceMethods.remove(MethodReference.getDefaultDispatchName(methodNode));
        }
        // all the remaining methods should be added under this interface to make sure we calling
        // all possible owners for this method.
        for (MethodReference methodReference : visitedInterfaceMethods.values()) {
            addNewMethod(
                    implementedInterface.getClassNode().name
                            + "."
                            + methodReference.getDefautlDispatchName(),
                    instrumentedClass,
                    implementedInterface.getClassNode(),
                    methodReference.method,
                    methods);
        }
        return null;
    }

    /**
     * Add a new method in the list of unseen methods in the instrumentedClass hierarchy.
     *
     * @param instrumentedClass class that is being visited
     * @param superClass        the class or interface defining the passed method.
     * @param method            the method to study
     * @param methods           the methods arlready encountered in the ClassNode hierarchy
     * @return the newly added {@link MethodReference} of null if the method was not added for any
     * reason.
     */
    @Nullable
    private static MethodReference addNewMethod(
            ClassNode instrumentedClass,
            ClassNode superClass,
            MethodNode method,
            Map<String, MethodReference> methods) {

        if (method.name.equals(ByteCodeUtils.CONSTRUCTOR)
                || method.name.equals(ByteCodeUtils.CLASS_INITIALIZER)) {
            return null;
        }

        String name = MethodReference.getDefaultDispatchName(method);

        // if we are dealing with an interface default method we should always provide a
        // way to invoke it through the access$super. It is possible that this default method
        // is overriden by a super class but since several interfaces can implement the same
        // method name, you can easily end up with a partial override (overriding some of the
        // implemented interfaces methods but not all of them).
        if ((superClass.access & Opcodes.ACC_INTERFACE) != 0
                && isParentClassVisible(superClass, instrumentedClass)) {
            // all default methods name will use the defining interface name for the hashcode
            // to avoid name collision when dealing with 2 methods with the same names coming
            // from 2 distinct interfaces.
            name = superClass.name + "." + name;
        }

        return addNewMethod(name, instrumentedClass, superClass, method, methods);
    }

    /**
     * Add a new method in the list of unseen methods in the instrumentedClass hierarchy.
     *
     * @param name              the dispatching name that will be used for matching callers to the passed method.
     * @param instrumentedClass class that is being visited
     * @param superClass        the class or interface defining the passed method.
     * @param method            the method to study
     * @param methods           the methods arlready encountered in the ClassNode hierarchy
     * @return the newly added {@link MethodReference} of null if the method was not added for any
     * reason.
     */
    @Nullable
    private static MethodReference addNewMethod(
            String name,
            ClassNode instrumentedClass,
            ClassNode superClass,
            MethodNode method,
            Map<String, MethodReference> methods) {
        if (isAccessCompatibleWithStark(method.access)
                && !methods.containsKey(name)
                && (method.access & Opcodes.ACC_STATIC) == 0
                && isCallableFromSubclass(method, superClass, instrumentedClass)) {
            MethodReference methodReference = new MethodReference(method, superClass);
            methods.put(name, methodReference);
            return methodReference;
        }
        return null;
    }

    @SuppressWarnings("SimplifiableIfStatement")
    private static boolean isCallableFromSubclass(
            @NonNull MethodNode method,
            @NonNull ClassNode superClass,
            @NonNull ClassNode subclass) {
        if ((method.access & Opcodes.ACC_PRIVATE) != 0) {
            return false;
        } else if ((method.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) {
            return true;
        } else {
            // "package private" access modifier.
            return ByteCodeUtils.getPackageName(superClass.name).equals(ByteCodeUtils.getPackageName(subclass.name));
        }
    }

    /**
     * Add all constructors from the passed ClassNode's methods. {@see ClassNode#methods}
     *
     * @param methods                 the constructors already encountered in the ClassNode hierarchy
     * @param classNode               the class to save all new methods from.
     * @param keepPrivateConstructors whether to keep the private constructors.
     */
    private void addAllNewConstructors(Map<String, MethodNode> methods, ClassNode classNode,
                                       boolean keepPrivateConstructors) {
        //noinspection unchecked
        for (MethodNode method : (List<MethodNode>) classNode.methods) {
            if (!method.name.equals(ByteCodeUtils.CONSTRUCTOR)) {
                continue;
            }

            if (!isAccessCompatibleWithStark(method.access)) {
                continue;
            }

            if (!keepPrivateConstructors && (method.access & Opcodes.ACC_PRIVATE) != 0) {
                continue;
            }
            if (!classNode.name.equals(visitedClassName)
                    && !classNode.name.equals(visitedSuperName)) {
                continue;
            }
            String key = classNode.name + "." + method.desc;
            if (methods.containsKey(key)) {
                continue;
            }
            methods.put(key, method);
        }
    }
}