/*
* JBoss, the OpenSource EJB server
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package javax.management.timer;

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;

import org.jboss.mx.util.RunnableScheduler;
import org.jboss.mx.util.SchedulableRunnable;
import org.jboss.logging.Logger;

/**
 * The timer service.
 *
 * @author <a href="mailto:Adrian.Brock@HappeningTimes.com">Adrian Brock</a>
 * @version $Revision: 1.14 $
 */
public class Timer
  extends NotificationBroadcasterSupport
  implements TimerMBean, MBeanRegistration
{
   // logging support
   private static Logger log = Logger.getLogger(Timer.class);

  // Constants -----------------------------------------------------

  /**
   * The number of milliseconds in one second.
   */
  public static final long ONE_SECOND = 1000;

  /**
   * The number of milliseconds in one minute.
   */
  public static final long ONE_MINUTE = ONE_SECOND * 60;

  /**
   * The number of milliseconds in one hour.
   */
  public static final long ONE_HOUR = ONE_MINUTE * 60;

  /**
   * The number of milliseconds in one day.
   */
  public static final long ONE_DAY = ONE_HOUR * 24;

  /**
   * The number of milliseconds in one week.
   */
  public static final long ONE_WEEK = ONE_DAY * 7;

  /**
   * Don't send notifications at initial start up.
   */
  private static final int SEND_NO = 0;

  /**
   * Send all past notifications at initial start up.
   */
  private static final int SEND_START = 1;

  /**
   * Normal operation sending
   */
  private static final int SEND_NORMAL = 2;

  // Attributes ----------------------------------------------------

  /**
   * The next notification id.
   */
  int nextId = 0;

  /**
   * The next notification sequence number.
   */
  long sequenceNumber = 0;

  /**
   * The send past events attribute.
   */
  boolean sendPastNotifications = false;

  /**
   * Whether the service is active.
   */
  boolean active = false;

  /**
   * Our object name.
   */
  ObjectName objectName;

  /**
   * The registered notifications.
   */
  HashMap notifications = new HashMap();

  /**
   * The scheduler
   */
  private RunnableScheduler scheduler = new RunnableScheduler();

  // Static --------------------------------------------------------

  // Constructors --------------------------------------------------

  // Public --------------------------------------------------------

  // TimerMBean implementation -------------------------------------

  public Integer addNotification(String type, String message, Object userData,
                                 Date date)
    throws IllegalArgumentException
  {
    return addNotification(type, message, userData, date, 0);
  }

  public Integer addNotification(String type, String message, Object userData,
                                 Date date, long period)
    throws IllegalArgumentException
  {
    return addNotification(type, message, userData, date, period, 0);
  }

  public Integer addNotification(String type, String message,
                Object userData, Date date, long period, long occurences)
    throws IllegalArgumentException
  {
     // Generate the next id.
    int newId = 0;
    newId = ++nextId;
    Integer id = new Integer(newId);

    // Validate and create the registration.
    RegisteredNotification rn =
      new RegisteredNotification(id, type, message, userData, date, period,
                                 occurences);

    // Add the registration.
    synchronized(notifications)
    {
       notifications.put(id, rn);
       rn.setNextRun(rn.nextDate);
       rn.setScheduler(scheduler);
    }

    return id;
  }

  public Vector getAllNotificationIDs()
  {
     synchronized(notifications)
     {
        return new Vector(notifications.keySet());
     }
  }

  public Date getDate(Integer id)
  {
    // Make sure there is a registration
    RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
    if (rn == null)
      return null;

    // Return a copy of the date.
    return new Date(rn.startDate);
  }

  public int getNbNotifications()
  {
    return notifications.size();
  }

  public Long getNbOccurences(Integer id)
  {
    // Make sure there is a registration
    RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
    if (rn == null)
      return null;

    // Return a copy of the occurences.
    return new Long(rn.occurences);
  }

   /**
    * Gets a copy of the flag indicating whether a peridic notification is executed at fixed-delay or at fixed-rate.
    *
    * @param id The timer notification identifier.
    * @return A copy of the flag indicating whether a peridic notification is executed at fixed-delay or at fixed-rate.
    */
   public Boolean getFixedRate(Integer id)
   {
      //[todo] implement getFixedRate
      throw new RuntimeException("NYI");
   }

   public Vector getNotificationIDs(String type)
  {
    Vector result = new Vector();

    // Loop through the notifications looking for the passed type.
    synchronized (notifications)
    {
       Iterator iterator = notifications.values().iterator();
       while (iterator.hasNext())
       {
          RegisteredNotification rn = (RegisteredNotification) iterator.next();
          if (rn.type.equals(type))
             result.add(rn.id);
       }
    }
      
    return result;
  }

  public String getNotificationMessage(Integer id)
  {
    // Make sure there is a registration
    RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
    if (rn == null)
      return null;

    // Return the message
    return rn.message;
  }

  public String getNotificationType(Integer id)
  {
    // Make sure there is a registration
    RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
    if (rn == null)
      return null;

    // Return the type.
    return rn.type;
  }

  public Object getNotificationUserData(Integer id)
  {
    // Make sure there is a registration
    RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
    if (rn == null)
      return null;

    // Return the user data.
    return rn.userData;
  }

  public Long getPeriod(Integer id)
  {
    // Make sure there is a registration
    RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
    if (rn == null)
      return null;

    // Return a copy of the period
    return new Long(rn.period);
  }

  public boolean getSendPastNotifications()
  {
    return sendPastNotifications;
  }

  public boolean isActive()
  {
    return active;
  }

  public boolean isEmpty()
  {
    return notifications.isEmpty();
  }

  public void removeAllNotifications()
  {
    // Remove the notifications
    synchronized(notifications)
    {
       Iterator iterator = notifications.values().iterator();
       while (iterator.hasNext())
       {
          RegisteredNotification rn = (RegisteredNotification) iterator.next();
          rn.setScheduler(null);
          iterator.remove();
       }
    }

    // The spec says to reset the identifiers, seems like a bad idea to me
    synchronized (this)
    {
       nextId = 0;
    }
  }

  public void removeNotification(Integer id)
    throws InstanceNotFoundException
  {

     log.debug("removeNotification: " + objectName + ",id=" + id);

    // Check if there is a notification.
    synchronized(notifications)
    {
       RegisteredNotification rn = (RegisteredNotification) notifications.get(id);
       if (rn == null)
         throw new InstanceNotFoundException("No notification id : " +
                                          id.toString());

       // Remove the notification
       rn.setScheduler(null);
       notifications.remove(id);
    }
  }

  public void removeNotifications(String type)
    throws InstanceNotFoundException
  {
    boolean found = false;

     log.debug("removeNotifications: " + objectName + ",type=" + type);

    // Loop through the notifications removing the passed type.
    synchronized(notifications)
    {
       Iterator iterator = notifications.values().iterator();
       while (iterator.hasNext())
       {
          RegisteredNotification rn = (RegisteredNotification) iterator.next();
          if (rn.type.equals(type))
          {
             rn.setScheduler(null);
             iterator.remove();
             found = true;
          }
       }
    }

    // The spec says to through an exception when nothing removed.
    if (found == false)
      throw new InstanceNotFoundException("Nothing registered for type: " +
                                          type);
  }

   public void setSendPastNotifications(boolean value)
   {
      log.debug("setSendPastNotifications: " + objectName + ",value=" + value);
      sendPastNotifications = value;
   }

   public synchronized void start()
   {
      // Ignore if already active
      if (active == true)
         return;
      active = true;

      log.debug("start: " + objectName + " at " + new Date());

      // Perform the initial sends, for past notifications send missed events
      // otherwise ignore them
      synchronized (notifications)
      {
         Iterator iterator = notifications.values().iterator();
         while (iterator.hasNext())
         {
            RegisteredNotification rn = (RegisteredNotification) iterator.next();
            if (sendPastNotifications)
               rn.sendType = SEND_START;
            else
               rn.sendType = SEND_NO;
            sendNotifications(rn);
            rn.sendType = SEND_NORMAL;
         }
      }

      // Start 'em up
      scheduler.start();
   }

  public synchronized void stop()
  {
    // Ignore if not active
    if (active == false)
      return;

     log.debug("stop: " + objectName + ",now=" + new Date());

    // Stop the threads
    active = false;
    scheduler.stop();
  }

   /**
    * Creates a new timer notification with the specified type, message and userData and inserts it into the list of notifications with a given date, period and number of occurences.
    * <p/>
    * If the timer notification to be inserted has a date that is before the current date, the method behaves as if the specified date were the current date.
    * For once-off notifications, the notification is delivered immediately.
    * For periodic notifications, the first notification is delivered immediately and the subsequent ones are spaced as specified by the period parameter.
    * <p/>
    * Note that once the timer notification has been added into the list of notifications, its associated date, period and number of occurences cannot be updated.
    * <p/>
    * In the case of a periodic notification, the value of parameter fixedRate is used to specify the execution scheme, as specified in Timer.
    *
    * @param type         The timer notification type.
    * @param message      The timer notification detailed message.
    * @param userData     The timer notification user data object.
    * @param date         The date when the notification occurs.
    * @param period       The period of the timer notification (in milliseconds).
    * @param nbOccurences The total number the timer notification will be emitted.
    * @param fixedRate    If true and if the notification is periodic, the notification is scheduled with a fixed-rate execution scheme. If false and if the notification is periodic, the notification is scheduled with a fixed-delay execution scheme. Ignored if the notification is not periodic.
    * @return The identifier of the new created timer notification.
    * @throws IllegalArgumentException The period or the number of occurences is negative
    */
   public Integer addNotification(String type, String message, Object userData, Date date, long period, long nbOccurences, boolean fixedRate) throws IllegalArgumentException
   {
      // [todo] implement addNotification
      throw new RuntimeException("NYI");
   }

   // MBeanRegistrationImplementation overrides ---------------------

  public ObjectName preRegister(MBeanServer server, ObjectName objectName)
    throws Exception
  {
    // Save the object name
    this.objectName = objectName;

    // Use the passed object name.
    return objectName;
  }

  public void postRegister(Boolean registrationDone)
  {
  }

  public void preDeregister()
    throws Exception
  {
    // Stop the timer before deregistration.
    stop();
  }

  public void postDeregister()
  {
  }

  // Package protected ---------------------------------------------

  // Protected -----------------------------------------------------

  // Private -------------------------------------------------------

  /**
   * Send any outstanding notifications.
   *
   * @param rn the registered notification to send.
   */
  private void sendNotifications(RegisteredNotification rn)
  {
     // Keep going until we have done all outstanding notifications.
     // The loop ends when not active, or there are no outstanding
     // notifications.
     // REVIEW: In practice for normal operation it never loops. We
     // ignore sends that we have missed. This avoids problems where
     // the notification takes longer than the period. Correct???
     while (isActive() && rn.nextDate != 0
             && rn.nextDate <= System.currentTimeMillis())
     {
        // Do we actually send it?
        // Yes, unless start and not sending past notifications.
        if (rn.sendType != SEND_NO)
        {
           long seq = 0;
           synchronized (this)
           {
              seq = ++sequenceNumber;
           }

           log.debug("sendNotification: " + rn);
           TimerNotification tn = new TimerNotification(rn.type, objectName,
              seq, rn.nextDate, rn.message, rn.id);
           tn.setUserData(rn.userData);
           sendNotification(tn);
        }
        // Calculate the next date.
        // Except for when we are sending past notifications at start up,
        // it cannot be in the future.
        do
        {
           // If no next run, remove it sets the next date to zero.
           if (rn.calcNextDate() == false)
           {
              synchronized (notifications)
              {
                 log.debug("remove: " + rn);
                 notifications.remove(rn.id);
              }
           }
        }
        while (isActive() && rn.sendType != SEND_START && rn.nextDate != 0
                && rn.occurences == 0 && rn.nextDate < System.currentTimeMillis());
     }

     if (rn.nextDate != 0)
        rn.setNextRun(rn.nextDate);
  }

  // Inner classes -------------------------------------------------

  /**
   * A registered notification. These run as separate threads.
   */
  private class RegisteredNotification
    extends SchedulableRunnable
  {
    // Attributes ----------------------------------------------------

    /**
     * The notification id.
     */
    public Integer id;

    /**
     * The notification type.
     */
    public String type;

    /**
     * The message.
     */
    public String message;

    /**
     * The user data.
     */
    public Object userData;

    /**
     * The start date.
     */
    public long startDate;

    /**
     * The period.
     */
    public long period;

    /**
     * The maximum number of occurences.
     */
    public long occurences;

    /**
     * The send type, no send, past notifications or normal
     */
    public int sendType = SEND_NORMAL;

    /**
     * The next run date
     */
    public long nextDate = 0;

    // Constructors --------------------------------------------------

    /**
     * The default constructor.
     *
     * @param id the notification id.
     * @param type the notification type.
     * @param message the notification's message string.
     * @param userData the notification's user data.
     * @param startDate the date/time the notification will occur.
     * @param period the repeat period in milli-seconds. Passing zero means
     *        no repeat.
     * @param occurences the maximum number of repeats. When the period is not
     *        zero and this parameter is zero, it will repeat indefinitely.
     * @exception IllegalArgumentException when the date is before the current
     *        date, the period is negative or the number of repeats is
     *        negative.
     */
    public RegisteredNotification(Integer id, String type, String message, Object userData, Date startDate, long period, long occurences)
            throws IllegalArgumentException
    {
       // Basic validation
       if (startDate == null)
          throw new IllegalArgumentException("Null Date");
       if (period < 0)
          throw new IllegalArgumentException("Negative Period");
       if (occurences < 0)
          throw new IllegalArgumentException("Negative Occurences");

       this.startDate = startDate.getTime();
       if (startDate.getTime() < System.currentTimeMillis())
       {
          log.warn("startDate [" + startDate + "] in the past, set to now");
          this.startDate = System.currentTimeMillis();
       }

       // Remember the values
       this.id = id;
       this.type = type;
       this.message = message;
       this.userData = userData;
       this.period = period;
       this.occurences = occurences;

       this.nextDate = this.startDate;

       String msgStr = "new " + this.toString();
       log.debug(msgStr);
    }

     // Public --------------------------------------------------------

     /**
      * Calculate the next notification date. Add on the period until
      * the number of occurences is exhausted.
      *
      * @return false when there are no more occurences, true otherwise.
      */
     boolean calcNextDate()
     {
        // No period, we've finished
        if (period == 0)
        {
           nextDate = 0;
           return false;
        }

        // Limited number of repeats have we finished?
        if (occurences != 0 && --occurences == 0)
        {
           nextDate = 0;
           return false;
        }

        // Calculate the next occurence
        nextDate += period;

        return true;
     }

     // SchedulableRunnable overrides ---------------------------------

     /**
      * Send the notifications.
      */
     public void doRun()
     {
        // Send any notifications
        sendNotifications(this);
     }

     public String toString()
     {
        return " RegisteredNotification: [timer=" + objectName + ",id=" + id + ",startDate=" + new Date(startDate) +
                ",periode=" + period + ",occurences=" + occurences + ",nextDate=" + new Date(nextDate) + "]";
     }
  }
}