/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.internal.util.concurrent;

import org.apache.geode.CancelCriterion;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.i18n.LocalizedStrings;

/**
 * Extends the CountDownLatch with the ability to also countUp.
 * <p>
 * Based on the original Doug Lea backport implementation of CountDownLatch.
 *
 * @see java.util.concurrent.CountDownLatch
 */
public class StoppableCountDownOrUpLatch {

  private int count_;

  /**
   * The cancellation criterion
   */
  private CancelCriterion stopper;

  /**
   * Constructs a <code>CountDownLatch</code> initialized with the given count.
   *
   * @param count the number of times {@link #countDown} must be invoked before threads can pass
   *        through {@link #await()}
   * @throws IllegalArgumentException if <code>count</count> is negative
   */
  public StoppableCountDownOrUpLatch(CancelCriterion stopper, int count) {
    Assert.assertTrue(stopper != null);
    if (count < 0)
      throw new IllegalArgumentException(
          LocalizedStrings.StoppableCountDownOrUpLatch_COUNT_0.toLocalizedString());
    this.stopper = stopper;
    this.count_ = count;
  }

  /**
   * Causes the current thread to wait until the latch has counted down to zero, unless the thread
   * is {@linkplain Thread#interrupt interrupted}.
   *
   * <p>
   * If the current count is zero then this method returns immediately.
   *
   * <p>
   * If the current count is greater than zero then the current thread becomes disabled for thread
   * scheduling purposes and lies dormant until one of two things happen:
   * <ul>
   * <li>The count reaches zero due to invocations of the {@link #countDown} method; or
   * <li>Some other thread {@linkplain Thread#interrupt interrupts} the current thread.
   * </ul>
   *
   * <p>
   * If the current thread:
   * <ul>
   * <li>has its interrupted status set on entry to this method; or
   * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
   * </ul>
   * then {@link InterruptedException} is thrown and the current thread's interrupted status is
   * cleared.
   *
   * @throws InterruptedException if the current thread is interrupted while waiting
   */
  public void await() throws InterruptedException {
    if (Thread.interrupted())
      throw new InterruptedException();
    // Modified to use inner primitive repeatedly, checking
    // for cancellation
    for (;;) {
      stopper.checkCancelInProgress(null);
      if (await(StoppableCountDownLatch.RETRY_TIME))
        break;
    }
  }

  private static final long NANOS_PER_MS = 1000000;

  /**
   * Causes the current thread to wait until the latch has counted down to zero, unless the thread
   * is {@linkplain Thread#interrupt interrupted}, or the specified waiting time elapses.
   *
   * <p>
   * If the current count is zero then this method returns immediately with the value
   * <code>true</code>.
   *
   * <p>
   * If the current count is greater than zero then the current thread becomes disabled for thread
   * scheduling purposes and lies dormant until one of three things happen:
   * <ul>
   * <li>The count reaches zero due to invocations of the {@link #countDown} method; or
   * <li>Some other thread {@linkplain Thread#interrupt interrupts} the current thread; or
   * <li>The specified waiting time elapses.
   * </ul>
   *
   * <p>
   * If the count reaches zero then the method returns with the value <code>true</code>.
   *
   * <p>
   * If the current thread:
   * <ul>
   * <li>has its interrupted status set on entry to this method; or
   * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
   * </ul>
   * then {@link InterruptedException} is thrown and the current thread's interrupted status is
   * cleared.
   *
   * <p>
   * If the specified waiting time elapses then the value <code>false</code> is returned. If the
   * time is less than or equal to zero, the method will not wait at all.
   *
   * @param msTimeout the maximum time to wait in milliseconds
   * @return <code>true</code> if the count reached zero and <code>false</code> if the waiting time
   *         elapsed before the count reached zero
   * @throws InterruptedException if the current thread is interrupted while waiting
   */
  public boolean await(long msTimeout) throws InterruptedException {
    if (Thread.interrupted())
      throw new InterruptedException();
    long nanos = msTimeout * NANOS_PER_MS; // millis to nanos
    synchronized (this) {
      if (count_ <= 0)
        return true;
      else if (nanos <= 0)
        return false;
      else {
        long deadline = System.nanoTime() + nanos;
        for (;;) {
          stopper.checkCancelInProgress(null);
          wait(nanos / NANOS_PER_MS, (int) (nanos % NANOS_PER_MS));
          if (count_ <= 0)
            return true;
          else {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0)
              return false;
          }
        }
      }
    }
  }

  /**
   * Decrements the count of the latch, releasing all waiting threads if the count reaches zero.
   *
   * <p>
   * If the current count is greater than zero then it is decremented. If the new count is zero then
   * all waiting threads are re-enabled for thread scheduling purposes.
   *
   * <p>
   * If the current count equals zero then nothing happens.
   */
  public synchronized void countDown() {
    if (count_ == 0)
      return;
    if (--count_ == 0)
      notifyAll();
  }

  /**
   * Returns the current count.
   *
   * <p>
   * This method is typically used for debugging and testing purposes.
   *
   * @return the current count
   */
  public long getCount() {
    return count_;
  }

  /**
   * Returns a string identifying this latch, as well as its state. The state, in brackets, includes
   * the String <code>"Count ="</code> followed by the current count.
   *
   * @return a string identifying this latch, as well as its state
   */
  @Override
  public String toString() {
    return super.toString() + "[Count = " + getCount() + "]";
  }

  /**
   * [GemStone addition]
   */
  public synchronized void countUp() {
    this.count_++;
  }

  /**
   * [GemStone addition]
   */
  public synchronized long getCountSync() {
    return getCount();
  }
}
