/*
 * 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.Constructor;
import java.lang.reflect.Method;

import java.time.Duration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.Stack;
import java.util.Vector;

import java.util.function.Supplier;

public class Test993 {

  public static final Breakpoint.Manager MANAGER = new Breakpoint.Manager();

  // A function we can use as a start breakpoint.
  public static void breakpoint() {
    return;
  }

  private static void privateBreakpoint() {
    return;
  }

  // An interface with a default method we can break on.
  static interface Breakable {
    public static void iBreakpoint() {
      return;
    }

    public default void breakit() {
      return;
    }
  }

  // A class that has a default method we breakpoint on.
  public static class TestClass1 implements Breakable {
    public TestClass1() {
      super();
    }
    public String toString() { return "TestClass1"; }
  }

  // A class that overrides a default method that we can breakpoint on and calls super.
  public static class TestClass1ext extends TestClass1 {
    public TestClass1ext() {
      super();
    }
    public String toString() { return "TestClass1Ext"; }
    public void breakit() {
      super.breakit();
    }
  }


  // A class that overrides a default method that we can breakpoint on.
  public static class TestClass2 implements Breakable {
    public String toString() { return "TestClass2"; }
    public void breakit() {
      return;
    }
  }

  // A class that overrides a default method that we can breakpoint on and calls super.
  public static class TestClass2ext extends TestClass2 {
    public String toString() { return "TestClass2ext"; }
    public void breakit() {
      super.breakit();
    }
  }

  // A class that overrides a default method and calls it directly with interface invoke-super
  public static class TestClass3 implements Breakable {
    public String toString() { return "TestClass3"; }
    public void breakit() {
      Breakable.super.breakit();
    }
  }

  // A class that overrides a default method that we can breakpoint on and calls super to a class
  // that uses interface-invoke-super.
  public static class TestClass3ext extends TestClass3 {
    public String toString() { return "TestClass3ext"; }
    public void breakit() {
      super.breakit();
    }
  }

  public static class TestClass4 {
    public String toString() { return "TestClass4"; }
    public void callPrivateMethod() {
      privateMethod();
    }
    private void privateMethod() {
      return;
    }
  }

  public static void notifyBreakpointReached(Thread thr, Executable e, long loc) {
    String line;
    if (e.getDeclaringClass().getPackage().equals(Test993.class.getPackage())) {
      line = Integer.valueOf(Breakpoint.locationToLine(e, loc)).toString();
    } else {
      line = "<NON-DETERMINISTIC>";
    }
    System.out.println("\t\t\tBreakpoint: " + e + " @ line=" + line);
  }

  public static interface ThrowRunnable extends Runnable {
    public default void run() {
      try {
        runThrow();
      } catch (Exception e) {
        throw new Error("Caught error while running " + this, e);
      }
    }
    public void runThrow() throws Exception;
  }

  public static class InvokeDirect implements Runnable {
    String msg;
    Runnable r;
    public InvokeDirect(String msg, Runnable r) {
      this.msg = msg;
      this.r = r;
    }
    @Override
    public void run() {
      System.out.println("\t\tInvoking \"" + msg + "\"");
      r.run();
    }
  }

  public static class InvokeReflect implements ThrowRunnable {
    Method m;
    Object this_arg;
    public InvokeReflect(Method m, Object this_arg) {
      this.m = m;
      this.this_arg = this_arg;
    }

    @Override
    public void runThrow() throws Exception {
      System.out.println("\t\tReflective invoking: " + m + " args: [this: " + this_arg + "]");
      m.invoke(this_arg);
    }
  }

  public static class InvokeNative implements Runnable {
    Method m;
    Object this_arg;
    public InvokeNative(Method m, Object this_arg) {
      this.m = m;
      this.this_arg = this_arg;
    }

    @Override
    public void run() {
      System.out.println("\t\tNative invoking: " + m + " args: [this: " + this_arg + "]");
      invokeNative(m, m.getDeclaringClass(), this_arg);
    }
  }

  public static native void invokeNative(Method m, Class<?> clazz, Object thizz);

  public static class InvokeNativeBool implements Runnable {
    Method m;
    Object this_arg;
    public InvokeNativeBool(Method m, Object this_arg) {
      this.m = m;
      this.this_arg = this_arg;
    }

    @Override
    public void run() {
      System.out.println("\t\tNative invoking: " + m + " args: [this: " + this_arg + "]");
      invokeNativeBool(m, m.getDeclaringClass(), this_arg);
    }
  }

  public static native void invokeNativeBool(Method m, Class<?> clazz, Object thizz);

  public static class InvokeNativeObject implements Runnable {
    Method m;
    Object this_arg;
    public InvokeNativeObject(Method m, Object this_arg) {
      this.m = m;
      this.this_arg = this_arg;
    }

    @Override
    public void run() {
      System.out.println("\t\tNative invoking: " + m + " args: [this: " + this_arg + "]");
      invokeNativeObject(m, m.getDeclaringClass(), this_arg);
    }
  }

  public static native void invokeNativeObject(Method m, Class<?> clazz, Object thizz);

  public static class InvokeNativeLong implements Runnable {
    Method m;
    Object this_arg;
    public InvokeNativeLong(Method m, Object this_arg) {
      this.m = m;
      this.this_arg = this_arg;
    }

    @Override
    public void run() {
      System.out.println("\t\tNative invoking: " + m + " args: [this: " + this_arg + "]");
      invokeNativeLong(m, m.getDeclaringClass(), this_arg);
    }
  }

  public static native void invokeNativeLong(Method m, Class<?> clazz, Object thizz);

  public static class ConstructDirect implements Runnable {
    String msg;
    Supplier<Object> s;
    public ConstructDirect(String msg, Supplier<Object> s) {
      this.msg = msg;
      this.s = s;
    }

    @Override
    public void run() {
      System.out.println("\t\tConstructing: " + msg);
      System.out.println("\t\t\tCreated: " + s.get());
    }
  }

  public static class ConstructReflect implements ThrowRunnable {
    Constructor<?> m;
    public ConstructReflect(Constructor<?> m) {
      this.m = m;
    }

    @Override
    public void runThrow() throws Exception {
      System.out.println("\t\tReflective constructor: " + m);
      System.out.println("\t\t\tCreated: " + m.newInstance());
    }
  }

  public static class ConstructNative implements Runnable {
    Constructor<?> m;
    Class type;
    public ConstructNative(Constructor<?> m) {
      this.m = m;
      this.type = m.getDeclaringClass();
    }

    @Override
    public void run() {
      System.out.println("\t\tNative constructor: " + m + ", type: " + type);
      System.out.println("\t\t\tCreated: " + constructNative(m, type));
    }
  }

  public static native Object constructNative(Constructor m, Class<?> clazz);

  private static <T> List<List<T>> combinations(List<T> items, int len) {
    if (len > items.size()) {
      throw new Error("Bad length" + len + " " + items);
    }
    if (len == 1) {
      List<List<T>> out = new ArrayList<>();
      for (T t : items) {
        out.add(Arrays.asList(t));
      }
      return out;
    }
    List<List<T>> out = new ArrayList<>();
    for (int rem = 0; rem <= items.size() - len; rem++) {
      for (List<T> others : combinations(items.subList(rem + 1, items.size()), len - 1)) {
        List<T> newone = new ArrayList<>();
        newone.add(items.get(rem));
        newone.addAll(others);
        out.add(newone);
      }
    }
    return out;
  }

  private static <T> List<List<T>> allCombinations(List<T> items) {
    List<List<T>> out = new ArrayList<List<T>>();
    out.add(new ArrayList<>());
    for (int i = 0; i < items.size(); i++) {
      out.addAll(combinations(items, i + 1));
    }
    return out;
  }

  private static Breakpoint.Manager.BP BP(Executable m) {
    return new Breakpoint.Manager.BP(m) {
      public String toString() {
        if (method.getDeclaringClass().getPackage().equals(Test993.class.getPackage())) {
          return super.toString();
        } else {
          return method.toString() + " @ <NON-DETERMINISTIC>";
        }
      }
    };
  }

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

    runMethodTests();
    runBCPMethodTests(test_BCP_private);
    runConstructorTests();

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

  public static void runConstructorTests() throws Exception {
    // The constructors we will be breaking on.
    Constructor<?> tc1_construct = TestClass1.class.getConstructor();
    Constructor<?> tc1ext_construct = TestClass1ext.class.getConstructor();

    Runnable[] tc1_constructors = new Runnable[] {
      new ConstructNative(tc1_construct),
      new ConstructReflect(tc1_construct),
      new ConstructDirect("new TestClass1()", TestClass1::new),
    };
    Breakpoint.Manager.BP[] tc1_bps = new Breakpoint.Manager.BP[] {
      BP(tc1_construct),
    };
    runTestGroups("TestClass1 constructor", tc1_constructors, tc1_bps);

    Runnable[] tc1ext_constructors = new Runnable[] {
      new ConstructNative(tc1ext_construct),
      new ConstructReflect(tc1ext_construct),
      new ConstructDirect("new TestClass1ext()", TestClass1ext::new),
    };
    Breakpoint.Manager.BP[] tc1ext_bps = new Breakpoint.Manager.BP[] {
      BP(tc1_construct), BP(tc1ext_construct),
    };
    runTestGroups("TestClass1ext constructor", tc1ext_constructors, tc1ext_bps);
  }

  // These test to make sure we are able to break on functions that might have been quickened or
  // inlined from the boot-image. These were all chosen for being in the bootclasspath, not being
  // long enough to prevent inlining, and not being used for the testing framework.
  public static void runBCPMethodTests(boolean test_BCP_private) throws Exception {
    // The methods we will be breaking on.
    Method bcp_private_method =
        test_BCP_private ? Duration.class.getDeclaredMethod("toBigDecimalSeconds") : null;
    Method bcp_virtual_method = Optional.class.getDeclaredMethod("isPresent");
    Method bcp_static_method = Optional.class.getDeclaredMethod("empty");
    Method bcp_private_static_method =
        test_BCP_private ? Random.class.getDeclaredMethod("seedUniquifier") : null;

    // Some constructors we will break on.
    Constructor<?> bcp_stack_constructor = Stack.class.getConstructor();
    Constructor<?> bcp_vector_constructor = Vector.class.getConstructor();
    if (!(Vector.class.isAssignableFrom(Stack.class))) {
      throw new Error("Expected Stack to extend Vector!");
    }

    // BCP constructors.
    Runnable[] vector_constructors = new Runnable[] {
      new ConstructNative(bcp_vector_constructor),
      new ConstructReflect(bcp_vector_constructor),
      new ConstructDirect("new Vector()", Vector::new),
    };
    Breakpoint.Manager.BP[] vector_breakpoints = new Breakpoint.Manager.BP[] {
      BP(bcp_vector_constructor),
    };
    runTestGroups("Vector constructor", vector_constructors, vector_breakpoints);

    Runnable[] stack_constructors = new Runnable[] {
      new ConstructNative(bcp_stack_constructor),
      new ConstructReflect(bcp_stack_constructor),
      new ConstructDirect("new Stack()", Stack::new),
    };
    Breakpoint.Manager.BP[] stack_breakpoints = new Breakpoint.Manager.BP[] {
      BP(bcp_stack_constructor), BP(bcp_vector_constructor),
    };
    runTestGroups("Stack constructor", stack_constructors, stack_breakpoints);

    // Static function
    Runnable[] static_invokes = new Runnable[] {
      new InvokeNativeObject(bcp_static_method, null),

      new InvokeReflect(bcp_static_method, null),

      new InvokeDirect("Optional::empty", () -> { Optional.empty(); }),
    };
    Breakpoint.Manager.BP[] static_breakpoints = new Breakpoint.Manager.BP[] {
      BP(bcp_static_method)
    };
    runTestGroups("bcp static invoke", static_invokes, static_breakpoints);

    // Static private class function
    if (test_BCP_private) {
      Runnable[] private_static_invokes = new Runnable[] {
        new InvokeNativeLong(bcp_private_static_method, null),

        new InvokeDirect("Random::seedUniquifier", () -> { new Random(); }),
      };
      Breakpoint.Manager.BP[] private_static_breakpoints = new Breakpoint.Manager.BP[] {
        BP(bcp_private_static_method)
      };
      runTestGroups("bcp private static invoke", private_static_invokes, private_static_breakpoints);
    }

    // private class method
    if (test_BCP_private) {
      Duration test_duration = Duration.ofDays(14);
      Runnable[] private_invokes = new Runnable[] {
        new InvokeNativeObject(bcp_private_method, test_duration),

        new InvokeDirect("Duration::toBigDecimalSeconds", () -> { test_duration.multipliedBy(2); }),
      };
      Breakpoint.Manager.BP[] private_breakpoints = new Breakpoint.Manager.BP[] {
        BP(bcp_private_method)
      };
      runTestGroups("bcp private invoke", private_invokes, private_breakpoints);
    }

    // class method
    Runnable[] public_invokes = new Runnable[] {
      new InvokeNativeBool(bcp_virtual_method, Optional.of("test")),

      new InvokeReflect(bcp_virtual_method, Optional.of("test2")),

      new InvokeDirect("Optional::isPresent", () -> { Optional.of("test3").isPresent(); }),
    };
    Breakpoint.Manager.BP[] public_breakpoints = new Breakpoint.Manager.BP[] {
      BP(bcp_virtual_method)
    };
    runTestGroups("bcp invoke", public_invokes, public_breakpoints);
  }

  public static void runMethodTests() throws Exception {
    // The methods we will be breaking on.
    Method breakpoint_method = Test993.class.getDeclaredMethod("breakpoint");
    Method private_breakpoint_method = Test993.class.getDeclaredMethod("privateBreakpoint");
    Method i_breakpoint_method = Breakable.class.getDeclaredMethod("iBreakpoint");
    Method breakit_method = Breakable.class.getDeclaredMethod("breakit");
    Method breakit_method_tc1ext = TestClass1ext.class.getDeclaredMethod("breakit");
    Method breakit_method_tc2 = TestClass2.class.getDeclaredMethod("breakit");
    Method breakit_method_tc2ext = TestClass2ext.class.getDeclaredMethod("breakit");
    Method breakit_method_tc3 = TestClass3.class.getDeclaredMethod("breakit");
    Method breakit_method_tc3ext = TestClass3ext.class.getDeclaredMethod("breakit");
    Method private_method = TestClass4.class.getDeclaredMethod("privateMethod");

    // Static class function
    Runnable[] static_invokes = new Runnable[] {
      new InvokeNative(breakpoint_method, null),

      new InvokeReflect(breakpoint_method, null),

      new InvokeDirect("Test993::breakpoint", Test993::breakpoint),
    };
    Breakpoint.Manager.BP[] static_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakpoint_method)
    };
    runTestGroups("static invoke", static_invokes, static_breakpoints);

    // Static private class function
    Runnable[] private_static_invokes = new Runnable[] {
      new InvokeNative(private_breakpoint_method, null),

      new InvokeDirect("Test993::privateBreakpoint", Test993::privateBreakpoint),
    };
    Breakpoint.Manager.BP[] private_static_breakpoints = new Breakpoint.Manager.BP[] {
      BP(private_breakpoint_method)
    };
    runTestGroups("private static invoke", private_static_invokes, private_static_breakpoints);

    // Static interface function.
    Runnable[] i_static_invokes = new Runnable[] {
      new InvokeNative(i_breakpoint_method, null),

      new InvokeReflect(i_breakpoint_method, null),

      new InvokeDirect("Breakable::iBreakpoint", Breakable::iBreakpoint),
    };
    Breakpoint.Manager.BP[] i_static_breakpoints = new Breakpoint.Manager.BP[] {
      BP(i_breakpoint_method)
    };
    runTestGroups("interface static invoke", i_static_invokes, i_static_breakpoints);

    // Call default method through a class.
    Runnable[] tc1_invokes = new Runnable[] {
      new InvokeNative(breakit_method, new TestClass1()),

      new InvokeReflect(breakit_method, new TestClass1()),

      new InvokeDirect("((Breakable)new TestClass1()).breakit()",
                  () -> ((Breakable)new TestClass1()).breakit()),
      new InvokeDirect("new TestClass1().breakit()",
                  () -> new TestClass1().breakit()),
    };
    Breakpoint.Manager.BP[] tc1_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakit_method)
    };
    runTestGroups("TestClass1 invokes", tc1_invokes, tc1_breakpoints);

    // Call default method through an override and normal invoke-super
    Runnable[] tc1ext_invokes = new Runnable[] {
      new InvokeNative(breakit_method, new TestClass1ext()),
      new InvokeNative(breakit_method_tc1ext, new TestClass1ext()),

      new InvokeReflect(breakit_method, new TestClass1ext()),
      new InvokeReflect(breakit_method_tc1ext, new TestClass1ext()),

      new InvokeDirect("((Breakable)new TestClass1ext()).breakit()",
                  () -> ((Breakable)new TestClass1ext()).breakit()),
      new InvokeDirect("((TestClass1)new TestClass1ext()).breakit()",
                  () -> ((TestClass1)new TestClass1ext()).breakit()),
      new InvokeDirect("new TestClass1ext().breakit()",
                  () -> new TestClass1ext().breakit()),
    };
    Breakpoint.Manager.BP[] tc1ext_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakit_method), BP(breakit_method_tc1ext)
    };
    runTestGroups("TestClass1ext invokes", tc1ext_invokes, tc1ext_breakpoints);

    // Override default/interface method.
    Runnable[] tc2_invokes = new Runnable[] {
      new InvokeNative(breakit_method, new TestClass2()),
      new InvokeNative(breakit_method_tc2, new TestClass2()),

      new InvokeReflect(breakit_method, new TestClass2()),
      new InvokeReflect(breakit_method_tc2, new TestClass2()),

      new InvokeDirect("((Breakable)new TestClass2()).breakit()",
                  () -> ((Breakable)new TestClass2()).breakit()),
      new InvokeDirect("new TestClass2().breakit()",
                  () -> new TestClass2().breakit()),
    };
    Breakpoint.Manager.BP[] tc2_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakit_method), BP(breakit_method_tc2)
    };
    runTestGroups("TestClass2 invokes", tc2_invokes, tc2_breakpoints);

    // Call overridden method using invoke-super
    Runnable[] tc2ext_invokes = new Runnable[] {
      new InvokeNative(breakit_method, new TestClass2ext()),
      new InvokeNative(breakit_method_tc2, new TestClass2ext()),
      new InvokeNative(breakit_method_tc2ext, new TestClass2ext()),

      new InvokeReflect(breakit_method, new TestClass2ext()),
      new InvokeReflect(breakit_method_tc2, new TestClass2ext()),
      new InvokeReflect(breakit_method_tc2ext, new TestClass2ext()),

      new InvokeDirect("((Breakable)new TestClass2ext()).breakit()",
                  () -> ((Breakable)new TestClass2ext()).breakit()),
      new InvokeDirect("((TestClass2)new TestClass2ext()).breakit()",
                  () -> ((TestClass2)new TestClass2ext()).breakit()),
      new InvokeDirect("new TestClass2ext().breakit())",
                  () -> new TestClass2ext().breakit()),
    };
    Breakpoint.Manager.BP[] tc2ext_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakit_method), BP(breakit_method_tc2), BP(breakit_method_tc2ext)
    };
    runTestGroups("TestClass2ext invokes", tc2ext_invokes, tc2ext_breakpoints);

    // Override default method and call it using interface-invoke-super
    Runnable[] tc3_invokes = new Runnable[] {
      new InvokeNative(breakit_method, new TestClass3()),
      new InvokeNative(breakit_method_tc3, new TestClass3()),

      new InvokeReflect(breakit_method, new TestClass3()),
      new InvokeReflect(breakit_method_tc3, new TestClass3()),

      new InvokeDirect("((Breakable)new TestClass3()).breakit()",
                  () -> ((Breakable)new TestClass3()).breakit()),
      new InvokeDirect("new TestClass3().breakit())",
                  () -> new TestClass3().breakit()),
    };
    Breakpoint.Manager.BP[] tc3_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakit_method), BP(breakit_method_tc3)
    };
    runTestGroups("TestClass3 invokes", tc3_invokes, tc3_breakpoints);

    // Call overridden method using invoke-super
    Runnable[] tc3ext_invokes = new Runnable[] {
      new InvokeNative(breakit_method, new TestClass3ext()),
      new InvokeNative(breakit_method_tc3, new TestClass3ext()),
      new InvokeNative(breakit_method_tc3ext, new TestClass3ext()),

      new InvokeReflect(breakit_method, new TestClass3ext()),
      new InvokeReflect(breakit_method_tc3, new TestClass3ext()),
      new InvokeReflect(breakit_method_tc3ext, new TestClass3ext()),

      new InvokeDirect("((Breakable)new TestClass3ext()).breakit()",
                  () -> ((Breakable)new TestClass3ext()).breakit()),
      new InvokeDirect("((TestClass3)new TestClass3ext()).breakit()",
                  () -> ((TestClass3)new TestClass3ext()).breakit()),
      new InvokeDirect("new TestClass3ext().breakit())",
                  () -> new TestClass3ext().breakit()),
    };
    Breakpoint.Manager.BP[] tc3ext_breakpoints = new Breakpoint.Manager.BP[] {
      BP(breakit_method), BP(breakit_method_tc3), BP(breakit_method_tc3ext)
    };
    runTestGroups("TestClass3ext invokes", tc3ext_invokes, tc3ext_breakpoints);

    // private instance method.
    Runnable[] private_instance_invokes = new Runnable[] {
      new InvokeNative(private_method, new TestClass4()),

      new InvokeDirect("new TestClass4().callPrivateMethod()",
                  () -> new TestClass4().callPrivateMethod()),
    };
    Breakpoint.Manager.BP[] private_instance_breakpoints = new Breakpoint.Manager.BP[] {
      BP(private_method)
    };
    runTestGroups(
        "private instance invoke", private_instance_invokes, private_instance_breakpoints);
  }

  private static void runTestGroups(String name,
                                    Runnable[] invokes,
                                    Breakpoint.Manager.BP[] breakpoints) throws Exception {
    System.out.println("Running " + name);
    for (List<Breakpoint.Manager.BP> bps : allCombinations(Arrays.asList(breakpoints))) {
      System.out.println("\tBreaking on " + bps);
      for (Runnable test : invokes) {
        MANAGER.clearAllBreakpoints();
        MANAGER.setBreakpoints(bps.toArray(new Breakpoint.Manager.BP[0]));
        test.run();
      }
    }
  }
}
