/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *      Copyright 2015 ForgeRock AS
 */
package org.opends.server.util;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import org.forgerock.util.Reject;

/**
 * Timer useful for testing: it helps to write loops that repeatedly runs code until some condition
 * is met.
 */
public interface TestTimer
{
  /**
   * Constant that can be used at the end of {@code Callable<Void>.call()} to better explicit this
   * is the end of the method.
   */
  Void END_RUN = null;

  /**
   * Repeatedly call the supplied callable (respecting a sleep interval) until:
   * <ul>
   * <li>it returns,</li>
   * <li>it throws an exception other than {@link AssertionError},</li>
   * <li>the current timer times out.</li>
   * </ul>
   * If the current timer times out, then it will:
   * <ul>
   * <li>either rethrow an {@link AssertionError} thrown by the callable,</li>
   * <li>or return {@code null}.</li>
   * </ul>
   * <p>
   * Note: The test code in the callable can be written as any test code outside a callable. In
   * particular, asserts can and should be used inside the {@link Callable#call()}.
   *
   * @param callable
   *          the callable to repeat until success
   * @param <R>
   *          The return type of the callable
   * @return the value returned by the callable (may be {@code null}), or {@code null} if the timer
   *         times out
   * @throws Exception
   *           The exception thrown by the provided callable
   * @throws InterruptedException
   *           If the thread is interrupted while sleeping
   */
  <R> R repeatUntilSuccess(Callable<R> callable) throws Exception, InterruptedException;

  /** Builder for a {@link TestTimer}. */
  public static final class Builder
  {
    private long maxSleepTimeInMillis;
    private long sleepTimes;

    /**
     * Configures the maximum sleep duration.
     *
     * @param time
     *          the duration
     * @param unit
     *          the time unit for the duration
     * @return this builder
     */
    public Builder maxSleep(long time, TimeUnit unit)
    {
      Reject.ifFalse(time > 0, "time must be positive");
      this.maxSleepTimeInMillis = unit.toMillis(time);
      return this;
    }

    /**
     * Configures the duration for sleep times.
     *
     * @param time
     *          the duration
     * @param unit
     *          the time unit for the duration
     * @return this builder
     */
    public Builder sleepTimes(long time, TimeUnit unit)
    {
      Reject.ifFalse(time > 0, "time must be positive");
      this.sleepTimes = unit.toMillis(time);
      return this;
    }

    /**
     * Creates a new timer and start it.
     *
     * @return a new timer
     */
    public TestTimer toTimer()
    {
      return new SteppingTimer(this);
    }
  }

  /** A {@link TestTimer} that sleeps in steps and sleeps at maximum {@code nbSteps * sleepTimes}. */
  public static class SteppingTimer implements TestTimer
  {
    private final long sleepTime;
    private final long totalNbSteps;
    private long nbStepsRemaining;

    private SteppingTimer(Builder builder)
    {
      this.sleepTime = builder.sleepTimes;
      this.totalNbSteps = sleepTime > 0 ? builder.maxSleepTimeInMillis / sleepTime : 0;
      this.nbStepsRemaining = totalNbSteps;
    }

    /**
     * Returns whether the timer has reached the timeout. This method may block by sleeping.
     *
     * @return {@code true} if the timer has reached the timeout, {@code false} otherwise
     * @throws InterruptedException if the thread has been interrupted
     */
    private boolean hasTimedOut() throws InterruptedException
    {
      final boolean done = hasTimedOutNoSleep();
      if (!done)
      {
        Thread.sleep(sleepTime);
        nbStepsRemaining--;
      }
      return done;
    }

    /**
     * Returns whether the timer has reached the timeout, without sleep.
     *
     * @return {@code true} if the timer has reached the timeout, {@code false} otherwise
     */
    private boolean hasTimedOutNoSleep()
    {
      return nbStepsRemaining <= 0;
    }

    @Override
    public <R> R repeatUntilSuccess(Callable<R> callable) throws Exception, InterruptedException
    {
      do
      {
        try
        {
          return callable.call();
        }
        catch (AssertionError e)
        {
          if (hasTimedOutNoSleep())
          {
            throw e;
          }
        }
      }
      while (!hasTimedOut());
      return null;
    }

    @Override
    public String toString()
    {
      return totalNbSteps * sleepTime + " ms max sleep time"
          + " (" + totalNbSteps + " steps x " + sleepTime + " ms)"
          + ", remaining = " + nbStepsRemaining + " steps";
    }
  }
}
