/*
** Module   : EventSemaphore.java
** Abstract : safely signal and wait on an event across threads 
**
** Copyright (c) 2009, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------Description------------------------
** 001 GES 20090312 Safely signal and wait on an event across threads.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.util;

import java.util.concurrent.*;

/**
 * Safely signal and wait on an event across threads. This class provides a
 * concurrency primitive that can be shared across multiple threads. When
 * created, it can be in a "set" state (threads that call {@link #waitFor}
 * will block) or it can be "posted" (the event has occurred and any threads
 * that call <code>waitFor</code> will immediately return). The event can
 * be triggered by calling {@link #post} which will release any currently
 * waiting threads (threads blocked in <code>waitFor</code>). Posting can
 * occur 0 or more times before the state is reset via {@link #reset}.
 * Reset causes the semaphore's state to be "set" again, no matter whether it
 * was previously in a set or posted state. A post count is kept which is the
 * number of times the event has been posted since the last reset. Each reset
 * does clear the post count to zero. The post count can be obtained with
 * {@link #query}.
 */
public class EventSemaphore
{
   /** The post count (number of posts since the last reset). */
   private int post = 1;
   
   /** Internal synchronization instance. */
   private Object lock = new Object();
   
   /** Blocking primitive for controlling waiting threads. */
   private CountDownLatch waiter = null;
   
   /**
    * Create an instance with its initial state as "set".
    */
   public EventSemaphore()
   {
      this(true);
   }
   
   /**
    * Create an instance.
    *
    * @param    set
    *           Initial state of the semaphore. <code>true</code> for the
    *           set state and <code>false</code> for posted.
    */
   public EventSemaphore(boolean set)
   {
      if (set)
         reset();
   }
   
   /**
    * Increment the post count, change state to "posted" if it was previously
    * "set" and release all waiting threads if the state changes.
    * <p>
    * The auto-reset option allows this to act as a one-time release (of any
    * currently waiting threads) but forcing the state back to "set" so that
    * subsequent calls to {@link #waitFor} will block.
    *
    * @param    reset
    *           <code>true</code> to force the semaphore's state back to "set"
    *           AFTER all currently waiting threads are released.
    */
   public void post(boolean reset)
   {
      synchronized (lock)
      {
         // remember if this is the post that must release threads
         boolean notify = (post == 0);
         
         // cache off the old blocking primitive
         CountDownLatch old = waiter;
         
         // manage our state
         if (reset)
         {
            // it is now OK to reset our state to "set" if needed
            reset();
         }
         else
         {
            // just increment the post count and ensure that no subsequent
            // threads will block
            post++;
            waiter = null;
         }
         
         // release the waiting threads
         if (old != null && notify)
            old.countDown();
      }
   }
   
   /**
    * Get the current post count.
    *
    * @return   The current post count. This will be zero if the state is "set"
    *           and will be some positive number if the state is "posted".
    */
   public int query()
   {
      int num = 0;
      
      synchronized (lock)
      {
         num = post;
      }
      
      return num;
   }
   
   /**
    * Change the state to "set", change the post count to zero and return the
    * previous post count. If the previous state was "posted", then after this
    * call, any calls to {@link #waitFor} will block until the next call to
    * {@link #post} releases them.
    *
    * @return   The post count before the state change was made. This will be
    *           zero if the state was "set" and will be some positive number if
    *           the state was "posted".
    */
   public int reset()
   {
      int num = 0;
      
      synchronized (lock)
      {
         num    = post;
         post   = 0;
         waiter = new CountDownLatch(1);
      }
      
      return num;
   }
   
   /**
    * Wait until the state is "posted" or the timeout occurs, whichever occurs
    * first. This will return immediately if the state is already "posted" on
    * entry.
    *
    * @param    timeout
    *           Number of milliseconds to wait for the event to be posted. Use
    *           0 for an indefinite wait. A negative number will be considered
    *           the same as 0.
    *
    * @return   <code>false</code> if a timeout occurred.
    */
   public boolean waitFor(long timeout)
   throws InterruptedException
   {
      CountDownLatch block = null;
      
      synchronized (lock)
      {
         block = waiter;
      }
      
      boolean result = true;
      
      // only block if there is a latch (otherwise the post count must be
      // positive)
      if (block != null)
      {
         if (timeout <= 0)
         {
            // indefinite path (never can timeout, will either throw an
            // InterruptedException or it will wait to be unblocked)
            block.await();
         }
         else
         {
            // timeout path (can also throw InterruptedException)
            result = block.await(timeout, TimeUnit.MILLISECONDS);
         }
      }
      
      return result;
   }
}
