/*
 * 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.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.LockSupport;
import java.util.ListIterator;
import java.util.function.Consumer;
import java.util.function.Function;

public class Test1931 {
  private static Monitors.LockController CONTENTION_SUPPRESSED = null;

  public static AutoCloseable SuppressContention(Monitors.LockController controller) {
    if (CONTENTION_SUPPRESSED != null) {
      throw new IllegalStateException("Only one contention suppression is possible at a time.");
    }
    CONTENTION_SUPPRESSED = controller;
    return () -> {
      CONTENTION_SUPPRESSED = null;
    };
  }

  public static void printStackTrace(Throwable t) {
    System.out.println("Caught exception: " + t);
    for (Throwable c = t.getCause(); c != null; c = c.getCause()) {
      System.out.println("\tCaused by: " +
          (Test1931.class.getPackage().equals(c.getClass().getPackage())
           ? c.toString() : c.getClass().toString()));
    }
  }

  public static void handleMonitorEnter(Thread thd, Object lock) {
    if (CONTENTION_SUPPRESSED != null && CONTENTION_SUPPRESSED.IsWorkerThread(thd)) {
      return;
    }
    System.out.println(thd.getName() + " contended-LOCKING " + lock);
  }

  public static void handleMonitorEntered(Thread thd, Object lock) {
    if (CONTENTION_SUPPRESSED != null && CONTENTION_SUPPRESSED.IsWorkerThread(thd)) {
      return;
    }
    System.out.println(thd.getName() + " LOCKED " + lock);
  }
  public static void handleMonitorWait(Thread thd, Object lock, long timeout) {
    System.out.println(thd.getName() + " start-monitor-wait " + lock + " timeout: " + timeout);
  }

  public static void handleMonitorWaited(Thread thd, Object lock, boolean timed_out) {
    System.out.println(thd.getName() + " monitor-waited " + lock + " timed_out: " + timed_out);
  }

  public static void run() throws Exception {
    Monitors.setupMonitorEvents(
        Test1931.class,
        Test1931.class.getDeclaredMethod("handleMonitorEnter", Thread.class, Object.class),
        Test1931.class.getDeclaredMethod("handleMonitorEntered", Thread.class, Object.class),
        Test1931.class.getDeclaredMethod("handleMonitorWait",
          Thread.class, Object.class, Long.TYPE),
        Test1931.class.getDeclaredMethod("handleMonitorWaited",
          Thread.class, Object.class, Boolean.TYPE),
        Monitors.NamedLock.class,
        null);

    System.out.println("Testing contended locking.");
    testLock(new Monitors.NamedLock("Lock testLock"));

    System.out.println("Testing park.");
    testPark(new Monitors.NamedLock("Parking blocker object"));

    System.out.println("Testing monitor wait.");
    testWait(new Monitors.NamedLock("Lock testWait"));

    System.out.println("Testing monitor timed wait.");
    testTimedWait(new Monitors.NamedLock("Lock testTimedWait"));

    System.out.println("Testing monitor timed with timeout.");
    testTimedWaitTimeout(new Monitors.NamedLock("Lock testTimedWaitTimeout"));

    // TODO It would be good (but annoying) to do this with jasmin/smali in order to test if it's
    // different without the reflection.
    System.out.println("Waiting on an unlocked monitor.");
    testUnlockedWait(new Monitors.NamedLock("Lock testUnlockedWait"));

    System.out.println("Waiting with an illegal argument (negative timeout)");
    testIllegalWait(new Monitors.NamedLock("Lock testIllegalWait"));

    System.out.println("Interrupt a monitor being waited on.");
    testInteruptWait(new Monitors.NamedLock("Lock testInteruptWait"));
  }

  public static void testPark(Object blocker) throws Exception {
    Thread holder = new Thread(() -> {
      LockSupport.parkNanos(blocker, 10); // Should round up to one millisecond
    }, "ParkThread");
    holder.start();
    holder.join();
  }

  public static void testInteruptWait(final Monitors.NamedLock lk) throws Exception {
    final Monitors.LockController controller1 = new Monitors.LockController(lk);
    controller1.DoLock();
    controller1.waitForLockToBeHeld();
    controller1.DoWait();
    controller1.waitForNotifySleep();
    try {
      controller1.interruptWorker();
      controller1.waitForLockToBeHeld();
      controller1.DoUnlock();
      System.out.println("No Exception thrown!");
    } catch (Monitors.TestException e) {
      printStackTrace(e);
    }
    controller1.DoCleanup();
  }

  public static void testIllegalWait(final Monitors.NamedLock lk) throws Exception {
    Monitors.LockController controller1 = new Monitors.LockController(lk, /*timed_wait time*/-100);
    controller1.DoLock();
    controller1.waitForLockToBeHeld();
    try {
      controller1.DoTimedWait();
      controller1.waitForNotifySleep();
      controller1.waitForLockToBeHeld();
      controller1.DoUnlock();
      System.out.println("No Exception thrown!");
    } catch (Monitors.TestException e) {
      printStackTrace(e);
    }
    controller1.DoCleanup();
  }

  public static void testUnlockedWait(final Monitors.NamedLock lk) throws Exception {
    synchronized (lk) {
      Thread thd = new Thread(() -> {
        try {
          Method m = Object.class.getDeclaredMethod("wait");
          m.invoke(lk);
        } catch (Exception e) {
          printStackTrace(e);
        }
      }, "Unlocked wait thread:");
      thd.start();
      thd.join();
    }
  }

  public static void testLock(Monitors.NamedLock lk) throws Exception {
    Monitors.LockController controller1 = new Monitors.LockController(lk);
    Monitors.LockController controller2 = new Monitors.LockController(lk);
    controller1.DoLock();
    controller1.waitForLockToBeHeld();
    controller2.DoLock();
    if (controller2.IsLocked()) {
      throw new Exception("c2 was able to gain lock while it was held by c1");
    }
    controller2.waitForContendedSleep();
    controller1.DoUnlock();
    controller2.waitForLockToBeHeld();
    controller2.DoUnlock();
  }

  public static void testWait(Monitors.NamedLock lk) throws Exception {
    Monitors.LockController controller1 = new Monitors.LockController(lk);
    Monitors.LockController controller2 = new Monitors.LockController(lk);
    controller1.DoLock();
    controller1.waitForLockToBeHeld();
    controller1.DoWait();
    controller1.waitForNotifySleep();
    try (AutoCloseable suppress = SuppressContention(controller2)) {
      // If controller1 has a spurious wakeup we could see contention here. Suppress it so it won't
      // cause the test to fail.
      controller2.DoLock();
      controller2.waitForLockToBeHeld();
      controller2.DoNotifyAll();
      controller2.DoUnlock();
    }
    controller1.waitForLockToBeHeld();
    controller1.DoUnlock();
  }

  public static void testTimedWait(Monitors.NamedLock lk) throws Exception {
    // Time to wait (1 hour). We will wake it up before timeout.
    final long millis =  60l * 60l * 1000l;
    Monitors.LockController controller1 = new Monitors.LockController(lk, millis);
    Monitors.LockController controller2 = new Monitors.LockController(lk);
    controller1.DoLock();
    controller1.waitForLockToBeHeld();
    controller1.DoTimedWait();
    controller1.waitForNotifySleep();
    try (AutoCloseable suppress = SuppressContention(controller2)) {
      // If controller1 has a spurious wakeup we could see contention here. Suppress it so it won't
      // cause the test to fail.
      controller2.DoLock();
      controller2.waitForLockToBeHeld();
      controller2.DoNotifyAll();
      controller2.DoUnlock();
    }
    controller1.waitForLockToBeHeld();
    controller1.DoUnlock();
  }

  public static void testTimedWaitTimeout(Monitors.NamedLock lk) throws Exception {
    // Time to wait (10 seconds). We will wait for the timeout.
    final long millis =  10l * 1000l;
    Monitors.LockController controller1 = new Monitors.LockController(lk, millis);
    controller1.DoLock();
    controller1.waitForLockToBeHeld();
    System.out.println("Waiting for 10 seconds.");
    controller1.DoTimedWait();
    controller1.waitForNotifySleep();
    controller1.DoUnlock();
    System.out.println("Wait finished with timeout.");
  }
}
