/*
 * Copyright DataStax, Inc.
 *
 * 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.datastax.oss.driver.api.testinfra.utils;

import static org.assertj.core.api.Fail.fail;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BooleanSupplier;

/**
 * @deprecated We've replaced this home-grown utility by Awaitility in our tests. We're preserving
 *     it because it was part of the public test infrastructure API, but it won't be maintained
 *     anymore, and removed in the next major version.
 * @see <a href="https://github.com/awaitility/awaitility">Awaitility homepage</a>
 */
@Deprecated
public class ConditionChecker {

  private static final int DEFAULT_PERIOD_MILLIS = 500;

  private static final int DEFAULT_TIMEOUT_MILLIS = 60000;

  /** @deprecated see {@link ConditionChecker} */
  @Deprecated
  public static class ConditionCheckerBuilder {

    private long timeout = DEFAULT_TIMEOUT_MILLIS;

    private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;

    private long period = DEFAULT_PERIOD_MILLIS;

    private TimeUnit periodUnit = TimeUnit.MILLISECONDS;

    private final Object predicate;

    private String description;

    ConditionCheckerBuilder(BooleanSupplier predicate) {
      this.predicate = predicate;
    }

    public ConditionCheckerBuilder(Runnable predicate) {
      this.predicate = predicate;
    }

    public ConditionCheckerBuilder every(long period, TimeUnit unit) {
      this.period = period;
      periodUnit = unit;
      return this;
    }

    public ConditionCheckerBuilder every(long periodMillis) {
      period = periodMillis;
      periodUnit = TimeUnit.MILLISECONDS;
      return this;
    }

    public ConditionCheckerBuilder before(long timeout, TimeUnit unit) {
      this.timeout = timeout;
      timeoutUnit = unit;
      return this;
    }

    public ConditionCheckerBuilder before(long timeoutMillis) {
      timeout = timeoutMillis;
      timeoutUnit = TimeUnit.MILLISECONDS;
      return this;
    }

    public ConditionCheckerBuilder as(String description) {
      this.description = description;
      return this;
    }

    public void becomesTrue() {
      new ConditionChecker(predicate, true, period, periodUnit, description)
          .await(timeout, timeoutUnit);
    }

    public void becomesFalse() {
      new ConditionChecker(predicate, false, period, periodUnit, description)
          .await(timeout, timeoutUnit);
    }
  }

  public static ConditionCheckerBuilder checkThat(BooleanSupplier predicate) {
    return new ConditionCheckerBuilder(predicate);
  }

  public static ConditionCheckerBuilder checkThat(Runnable predicate) {
    return new ConditionCheckerBuilder(predicate);
  }

  private final Object predicate;
  private final boolean expectedOutcome;
  private final String description;
  private final Lock lock;
  private final Condition condition;
  private final Timer timer;
  private Throwable lastFailure;

  public ConditionChecker(
      Object predicate,
      boolean expectedOutcome,
      long period,
      TimeUnit periodUnit,
      String description) {
    this.predicate = predicate;
    this.expectedOutcome = expectedOutcome;
    this.description = (description != null) ? description : this.toString();
    lock = new ReentrantLock();
    condition = lock.newCondition();
    timer = new Timer("condition-checker", true);
    timer.schedule(
        new TimerTask() {
          @Override
          public void run() {
            checkCondition();
          }
        },
        0,
        periodUnit.toMillis(period));
  }

  /** Waits until the predicate becomes true, or a timeout occurs, whichever happens first. */
  public void await(long timeout, TimeUnit unit) {
    boolean interrupted = false;
    long nanos = unit.toNanos(timeout);
    lock.lock();
    try {
      while (!evalCondition()) {
        if (nanos <= 0L) {
          String msg =
              String.format(
                  "Timeout after %s %s while waiting for '%s'",
                  timeout, unit.toString().toLowerCase(), description);
          if (lastFailure != null) {
            fail(msg, lastFailure);
          } else {
            fail(msg);
          }
        }
        try {
          nanos = condition.awaitNanos(nanos);
        } catch (InterruptedException e) {
          interrupted = true;
        }
      }
    } finally {
      timer.cancel();
      if (interrupted) Thread.currentThread().interrupt();
    }
  }

  private void checkCondition() {
    lock.lock();
    try {
      if (evalCondition()) {
        condition.signal();
      }
    } finally {
      lock.unlock();
    }
  }

  private boolean evalCondition() {
    if (predicate instanceof BooleanSupplier) {
      return ((BooleanSupplier) predicate).getAsBoolean() == expectedOutcome;
    } else if (predicate instanceof Runnable) {
      boolean succeeded = true;
      try {
        ((Runnable) predicate).run();
      } catch (Throwable t) {
        succeeded = false;
        lastFailure = t;
      }
      return succeeded == expectedOutcome;
    } else {
      throw new AssertionError("Unsupported predicate type " + predicate.getClass());
    }
  }
}
