/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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 art;

import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.Base64;

public class Test996 {
    // The line we are going to break on. This should be the println in the Transform class.
    // We set a breakpoint here after we have redefined the class.
    public static final int TRANSFORM_BREAKPOINT_REDEFINED_LINE = 40;

    // The line we initially set a breakpoint on. This should be the doNothing call. This should be
    // cleared by the redefinition and should only be caught on the initial run.
    public static final int TRANSFORM_BREAKPOINT_INITIAL_LINE = 42;

    // A function that doesn't do anything. Used for giving places to break on in a function.
    public static void doNothing() {}

    public static final class Transform {
        public void run(Runnable r) {
            r.run();
            // Make sure we don't change anything above this line to keep all the breakpoint stuff
            // working. We will be putting a breakpoint before this line in the runnable.
            System.out.println("Should be after first breakpoint.");
            // This is set as a breakpoint prior to redefinition. It should not be hit.
            doNothing();
        }
    }

    /* ****************************************************************************************** */
    // Try to keep all edits to this file below the above line. If edits need to be made above this
    // line be sure to update the TRANSFORM_BREAKPOINT_REDEFINED_LINE and
    // TRANSFORM_BREAKPOINT_INITIAL_LINE to their appropriate values.

    public static final int TRANSFORM_BREAKPOINT_POST_REDEFINITION_LINE = 8;

    // The base64 encoding of the following class. The redefined 'run' method should have the same
    // instructions as the original. This means that the locations of each line should stay the same
    // and the set of valid locations will not change. We use this to ensure that breakpoints are
    // removed from the redefined method.
    // public static final class Transform {
    //     public void run(Runnable r) {
    //         r.run();
    //         System.out.println("Doing nothing transformed");
    //         doNothing();  // try to catch non-removed breakpoints
    //     }
    // }
    private static final byte[] CLASS_BYTES = Base64.getDecoder().decode(
            "yv66vgAAADQAKAoACAARCwASABMJABQAFQgAFgoAFwAYCgAZABoHABsHAB4BAAY8aW5pdD4BAAMo" +
            "KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQADcnVuAQAXKExqYXZhL2xhbmcvUnVubmFibGU7" +
            "KVYBAApTb3VyY2VGaWxlAQAMVGVzdDk5Ni5qYXZhDAAJAAoHAB8MAA0ACgcAIAwAIQAiAQAZRG9p" +
            "bmcgbm90aGluZyB0cmFuc2Zvcm1lZAcAIwwAJAAlBwAmDAAnAAoBABVhcnQvVGVzdDk5NiRUcmFu" +
            "c2Zvcm0BAAlUcmFuc2Zvcm0BAAxJbm5lckNsYXNzZXMBABBqYXZhL2xhbmcvT2JqZWN0AQASamF2" +
            "YS9sYW5nL1J1bm5hYmxlAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50" +
            "U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3Ry" +
            "aW5nOylWAQALYXJ0L1Rlc3Q5OTYBAAlkb05vdGhpbmcAMQAHAAgAAAAAAAIAAQAJAAoAAQALAAAA" +
            "HQABAAEAAAAFKrcAAbEAAAABAAwAAAAGAAEAAAAEAAEADQAOAAEACwAAADYAAgACAAAAEiu5AAIB" +
            "ALIAAxIEtgAFuAAGsQAAAAEADAAAABIABAAAAAYABgAHAA4ACAARAAkAAgAPAAAAAgAQAB0AAAAK" +
            "AAEABwAZABwAGQ==");
    private static final byte[] DEX_BYTES = Base64.getDecoder().decode(
            "ZGV4CjAzNQBzn3TiKGAiM0fubj25v816W0k+niqj+SQcBAAAcAAAAHhWNBIAAAAAAAAAAFgDAAAW" +
            "AAAAcAAAAAoAAADIAAAAAwAAAPAAAAABAAAAFAEAAAYAAAAcAQAAAQAAAEwBAACwAgAAbAEAANoB" +
            "AADiAQAA/QEAABYCAAAlAgAASQIAAGkCAACAAgAAlAIAAKoCAAC+AgAA0gIAAOACAADrAgAA7gIA" +
            "APICAAD/AgAACgMAABADAAAVAwAAHgMAACMDAAACAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAA" +
            "CQAAAAoAAAANAAAADQAAAAkAAAAAAAAADgAAAAkAAADMAQAADgAAAAkAAADUAQAACAAEABIAAAAA" +
            "AAAAAAAAAAAAAQAUAAAAAQAAABAAAAAEAAIAEwAAAAUAAAAAAAAABgAAABQAAAAAAAAAEQAAAAUA" +
            "AAAAAAAACwAAALwBAABHAwAAAAAAAAIAAAA4AwAAPgMAAAEAAQABAAAAKgMAAAQAAABwEAQAAAAO" +
            "AAQAAgACAAAALwMAAA4AAAByEAUAAwBiAAAAGgEBAG4gAwAQAHEAAgAAAA4AbAEAAAAAAAAAAAAA" +
            "AAAAAAEAAAAGAAAAAQAAAAcABjxpbml0PgAZRG9pbmcgbm90aGluZyB0cmFuc2Zvcm1lZAAXTGFy" +
            "dC9UZXN0OTk2JFRyYW5zZm9ybTsADUxhcnQvVGVzdDk5NjsAIkxkYWx2aWsvYW5ub3RhdGlvbi9F" +
            "bmNsb3NpbmdDbGFzczsAHkxkYWx2aWsvYW5ub3RhdGlvbi9Jbm5lckNsYXNzOwAVTGphdmEvaW8v" +
            "UHJpbnRTdHJlYW07ABJMamF2YS9sYW5nL09iamVjdDsAFExqYXZhL2xhbmcvUnVubmFibGU7ABJM" +
            "amF2YS9sYW5nL1N0cmluZzsAEkxqYXZhL2xhbmcvU3lzdGVtOwAMVGVzdDk5Ni5qYXZhAAlUcmFu" +
            "c2Zvcm0AAVYAAlZMAAthY2Nlc3NGbGFncwAJZG9Ob3RoaW5nAARuYW1lAANvdXQAB3ByaW50bG4A" +
            "A3J1bgAFdmFsdWUABAAHDgAGAQAHDjx4PAACAgEVGAECAwIPBBkRFwwAAAEBAIGABPgCAQGQAwAA" +
            "ABAAAAAAAAAAAQAAAAAAAAABAAAAFgAAAHAAAAACAAAACgAAAMgAAAADAAAAAwAAAPAAAAAEAAAA" +
            "AQAAABQBAAAFAAAABgAAABwBAAAGAAAAAQAAAEwBAAADEAAAAQAAAGwBAAABIAAAAgAAAHgBAAAG" +
            "IAAAAQAAALwBAAABEAAAAgAAAMwBAAACIAAAFgAAANoBAAADIAAAAgAAACoDAAAEIAAAAgAAADgD" +
            "AAAAIAAAAQAAAEcDAAAAEAAAAQAAAFgDAAA=");

    public static void notifyBreakpointReached(Thread thr, Executable e, long loc) {
        int line = Breakpoint.locationToLine(e, loc);
        if (line == -1 && e.getName().equals("run")
                && e.getDeclaringClass().equals(Transform.class)) {
            // RI always reports line = -1 for obsolete methods. Just replace it with the real line
            // for consistency.
            line = TRANSFORM_BREAKPOINT_REDEFINED_LINE;
        }
        System.out.println("Breakpoint reached: " + e + " @ line=" + line);
    }

    public static void run() throws Exception {
        // Set up breakpoints
        Breakpoint.stopBreakpointWatch(Thread.currentThread());
        Breakpoint.startBreakpointWatch(
                Test996.class,
                Test996.class.getDeclaredMethod(
                        "notifyBreakpointReached", Thread.class, Executable.class, Long.TYPE),
                Thread.currentThread());

        Transform t = new Transform();
        Method non_obsolete_run_method = Transform.class.getDeclaredMethod("run", Runnable.class);
        final long obsolete_breakpoint_location = Breakpoint.lineToLocation(
                non_obsolete_run_method, TRANSFORM_BREAKPOINT_REDEFINED_LINE);

        System.out.println(
                "Initially setting breakpoint to line " + TRANSFORM_BREAKPOINT_INITIAL_LINE);
        long initial_breakpoint_location = Breakpoint.lineToLocation(
                non_obsolete_run_method, TRANSFORM_BREAKPOINT_INITIAL_LINE);
        Breakpoint.setBreakpoint(non_obsolete_run_method, initial_breakpoint_location);

        System.out.println("Running transform without redefinition.");
        t.run(() -> {});

        System.out.println("Running transform with redefinition.");
        t.run(() -> {
            System.out.println("Redefining calling function!");
            // This should clear the breakpoint set to TRANSFORM_BREAKPOINT_INITIAL_LINE
            Redefinition.doCommonClassRedefinition(Transform.class, CLASS_BYTES, DEX_BYTES);
            System.out.println("Setting breakpoint on now obsolete method to line " +
                    TRANSFORM_BREAKPOINT_REDEFINED_LINE);
            setBreakpointOnObsoleteMethod(obsolete_breakpoint_location);
        });
        System.out.println("Running transform post redefinition. Should not hit any breakpoints.");
        t.run(() -> {});

        System.out.println("Setting initial breakpoint on redefined method.");
        long final_breakpoint_location = Breakpoint.lineToLocation(
                non_obsolete_run_method, TRANSFORM_BREAKPOINT_POST_REDEFINITION_LINE);
        Breakpoint.setBreakpoint(non_obsolete_run_method, final_breakpoint_location);
        t.run(() -> {});

        Breakpoint.stopBreakpointWatch(Thread.currentThread());
    }

    public static native void setBreakpointOnObsoleteMethod(long location);
}
