/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.varia.scheduler;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;

import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.NotificationEmitter;
import javax.management.ObjectName;
import javax.management.timer.TimerNotification;
import javax.management.timer.TimerMBean;
import javax.management.timer.Timer;

import EDU.oswego.cs.dl.util.concurrent.SynchronizedInt;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap;
import org.jboss.logging.Logger;
import org.jboss.system.ServiceMBeanSupport;

/**
 * ScheduleManager manages multiple scheduled timer listeners.
 * These are registered using the {@link #addSchedule} operation.
 * Providers (basically MBean lifecycle listeners) can be registered using the
 * {@link #registerProvider} operation.
 *
 * Because of the way the JBoss deployment model works (no way to readily
 * invoke operations at deployment time), prefer to use the {@link Scheduler}
 * MBean instead.
 *
 * @author <a href="mailto:andreas@jboss.org">Andreas Schaefer</a>
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 * @version $Revision: 81038 $
 */
public class ScheduleManager extends ServiceMBeanSupport
   implements ScheduleManagerMBean
{

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

   /** 
	* Default Timer Object Name
	*/
   public static String DEFAULT_TIMER_NAME = "jboss:service=Timer";

   /** 
	* Counter for the number of scheduled instances.
	*/
   private static SynchronizedInt sCounter = new SynchronizedInt(0);

   private static final int NOTIFICATION = 0;
   private static final int DATE = 1;
   private static final int REPETITIONS = 2;
   private static final int SCHEDULER_NAME = 3;
   private static final int NULL = 4;
   private static final int ID = 5;
   private static final int NEXT_DATE = 6;

   // -------------------------------------------------------------------------
   // Members
   // -------------------------------------------------------------------------

   private String mTimerName = DEFAULT_TIMER_NAME;
   private ObjectName mTimerObjectName;
   private TimerMBean mTimer;
   private NotificationEmitter mTimerEmitter;

   private boolean mStartOnStart = true;
   private boolean mFixedRate = false;
   private SynchronizedBoolean mIsPaused = new SynchronizedBoolean(false);

   /**
	* List of registered AbstractScheduleProvider ObjectNames to inform 
	* when the this manager is stop or started.
    */
   private List mProviders = Collections.synchronizedList(new ArrayList());
   
   /** 
	* Maps Integer to registered ScheduleInstance.
	*/
   private Map mSchedules = new ConcurrentReaderHashMap();

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

   /**
    * Default (no-args) Constructor
    */
   public ScheduleManager()
   {
   }

   // -------------------------------------------------------------------------
   // SchedulerMBean Methods
   // -------------------------------------------------------------------------

   /**
    * Starts all the registered Schedules
    *
    * @jmx:managed-operation
    */
   public void startSchedules()
   {
      log.debug("startSchedules()");
      // Check if not already started
      if (!isStarted())
      {
         // Loop over all available Schedule Instance and start them now
         Iterator i = mSchedules.values().iterator();
         while (i.hasNext())
         {
            ScheduleInstance lInstance = (ScheduleInstance) i.next();
            try
            {
               lInstance.start();
            }
            catch (JMException e)
            {
               log.error("Could not start: " + lInstance, e);
            }
         }
      }
   }

   /**
    * Stops all the registered Schedules
    *
    * @jmx:managed-operation
    *
    * @param pDoItNow If true the schedule will be stopped without waiting for the next
    *                 scheduled call otherwise the next call will be performed before
    *                 the schedule is stopped.
    */
   public void stopSchedules(boolean pDoItNow)
   {
      // Check if it is already started
      if (isStarted())
      {
         // Loop over all available Schedule Instance and start them now
         Iterator i = mSchedules.values().iterator();
         while (i.hasNext())
         {
            ScheduleInstance lInstance = (ScheduleInstance) i.next();
            try
            {
               lInstance.stop();
            }
            catch (JMException e)
            {
               log.error("Could not stop: " + lInstance, e);
            }
         }
      }
   }

   /**
    * Stops existing schedules immediately and restarts them right away.
    *
    * @jmx:managed-operation
    */
   public void restartSchedule()
   {
      stopSchedules(true);
      startSchedules();
   }

   /**
    * Register a Provider to make it available. In turn this
    * method calls "startProviding()" method on the Provider
    * to indicate that the Provider can start adding Schedules.
    *
    * @param pProviderObjectName Object Name of the Provider
    *
    * @jmx:managed-operation
    */
   public void registerProvider(String pProviderObjectName)
   {
	  try
	  {
		 registerProvider(new ObjectName(pProviderObjectName));
	  }
      catch (JMException jme)
      {
         log.error("Could not call startProviding() on " + pProviderObjectName, jme);
      }
   }

   /**
    * Register a Provider to make it available. In turn this
    * method calls "startProviding()" method on the Provider
    * to indicate that the Provider can start adding Schedules.
    *
    * @param pProviderObjectName Object Name of the Provider
    *
    * @jmx:managed-operation
    */
   public void registerProvider(ObjectName pProviderObjectName)
	  throws JMException
   {
      if (pProviderObjectName == null)
      {
         throw new MalformedObjectNameException("Provider must not be null");
      }
	  synchronized (mProviders) {
		 if (mProviders.contains(pProviderObjectName))
			throw new JMException("Already registered: " + pProviderObjectName);
		 mProviders.add(pProviderObjectName);
	  }
	  server.invoke(
			  pProviderObjectName,
			  "startProviding",
			  new Object[]{},
			  new String[]{}
	  );
   }

   /**
    * Unregister a Provider which in turn calls "stopProviding()"
    * indicating to the Provider to remove all the Schedules.
    *
    * @param pProviderObjectName Object Name of the Provider
    *
    * @jmx:managed-operation
    */
   public void unregisterProvider(String pProviderObjectName)
   {
      try
      {
		 unregisterProvider(new ObjectName(pProviderObjectName));
      }
      catch (JMException jme)
      {
         log.error("Could not call stopProviding() on " + pProviderObjectName, jme);
      }
   }

   /**
    * Unregister a Provider which in turn calls "stopProviding()"
    * indicating to the Provider to remove all the Schedules.
    *
    * @param pProviderObjectName Object Name of the Provider
    *
    * @jmx:managed-operation
    */
   public void unregisterProvider(ObjectName pProviderObjectName)
	  throws JMException
   {
      if (!mProviders.remove(pProviderObjectName))
		 return;
	  server.invoke(
			  pProviderObjectName,
			  "stopProviding",
			  new Object[]{},
			  new String[]{}
	  );
   }

   /**
    * Adds a new Schedule to the Scheduler
    *
    * @param pTarget Object Name of the Target MBean
    * @param pMethodName Name of the method to be called
    * @param pMethodSignature List of Attributes of the method to be called
    *                         where ...
    * @param pStartDate Date when the schedule is started
    * @param pRepetitions Initial Number of repetitions
    *
    * @return Identification of the Schedule used later to remove it
    *         if necessary
    *
    * @jmx:managed-operation
    */
   public int addSchedule(
           ObjectName pProvider,
           ObjectName pTarget,
           String pMethodName,
           String[] pMethodSignature,
           Date pStartDate,
           long pPeriod,
           int pRepetitions
           )
   {
      ScheduleInstance lInstance = new ScheduleInstance(
              pProvider,
              pTarget,
              pMethodName,
              pMethodSignature,
              pStartDate,
              pRepetitions,
              pPeriod
      );
      if (isStarted())
      {
         try
         {
            lInstance.start();
         }
         catch (JMException jme)
         {
            log.error("Could not start " + lInstance, jme);
         }
      }
      int lID = lInstance.getID();
      mSchedules.put(new Integer(lID), lInstance);
      return lID;
   }

   /**
    * Removes a Schedule so that no notification is sent anymore
    *
    * @param pIdentification Identification returned by {@link #addSchedule
    *                        addSchedule()} or {@link #getSchedules
    *                        getSchedules()}.
    *
    * @jmx:managed-operation
    */
   public void removeSchedule(int pIdentification)
   {
      ScheduleInstance lInstance = (ScheduleInstance) mSchedules.get(new Integer(pIdentification));
      try
      {
		 if (lInstance == null)
			throw new InstanceNotFoundException();
         lInstance.stop();
      }
      catch (JMException e)
      {
         log.error("Could not stop " + lInstance, e);
      }
      mSchedules.remove(new Integer(pIdentification));
   }

   /**
    * Returns a list of the ids of all registered schedules
    *
    * @return List of Ids separated by a ","
    */
   public String getSchedules()
   {
      Iterator i = mSchedules.values().iterator();
      StringBuffer lReturn = new StringBuffer();
      boolean lFirst = true;
      while (i.hasNext())
      {
         ScheduleInstance lInstance = (ScheduleInstance) i.next();
         if (lFirst)
         {
            lReturn.append(lInstance.mIdentification);
            lFirst = false;
         }
         else
         {
            lReturn.append(",").append(lInstance.mIdentification);
         }
      }
      return lReturn.toString();
   }

   /**
    * @return True if all the Schedules are paused meaning that even when the notifications
    *   are sent to the listener they are ignored. ATTENTION: this applies to all registered
    *   Schedules and any notifications are lost during pausing
    */
   public boolean isPaused()
   {
      return mIsPaused.get();
   }

   /**
    * Pauses or restarts the Schedules which either suspends the
    * notifications or start transfering them to the target
    *
    * @param pIsPaused True when the Schedules are paused or false when they resume
    */
   public void setPaused(boolean pIsPaused)
   {
      mIsPaused.set(pIsPaused);
   }

   /**
    * @return true if the Schedule Manager is started
    */
   public boolean isStarted()
   {
      return getState() == STARTED;
   }

   // -------------------------------------------------------------------------
   // SchedulerManagerMBean Attributes
   // -------------------------------------------------------------------------

   /**
    * Set the scheduler to start when MBean started or not. Note that this method only
    * affects when the {@link #startService startService()} gets called (normally at
    * startup time.
    *
    * @jmx:managed-attribute
    *
    * @param pStartAtStartup if the scheduler should be started upon MBean start or not
    */
   public void setStartAtStartup(boolean pStartAtStartup)
   {
      mStartOnStart = pStartAtStartup;
   }

   /**
    * @jmx:managed-attribute
    *
    * @return true if the scheduler should be started upon MBean start or not
    */
   public boolean isStartAtStartup()
   {
      return mStartOnStart;
   }
   
   /**
    * @jmx:managed-attribute
    *
    * @param pTimerName Object Name of the Timer MBean to
    *                   be used. If null or not a valid ObjectName
    *                   the default will be used
    */
   public void setTimerName(String pTimerName)
   {
      mTimerName = pTimerName;
   }
   
   /**
    * @jmx:managed-attribute
    *
    * @return Name of the Timer MBean used in here
    */
   public String getTimerName()
   {
      return mTimerName;
   }

   /**
    * @jmx:managed-attribute
    * 
    * @param fixedRate the default scheduling to use, fixed-rate or fixed-delay (false, default)
    */
   public void setFixedRate(boolean fixedRate)
   {
      mFixedRate = fixedRate;
   }
   
   /**
    * @jmx:managed-attribute
    * 
    * @return the default scheduling to use
    */   
   public boolean getFixedRate()
   {
      return mFixedRate;
   }
   
   // -------------------------------------------------------------------------
   // ServiceMBeanSupport overrides
   // -------------------------------------------------------------------------

   public ObjectName getObjectName(MBeanServer pServer, ObjectName pName)
      throws MalformedObjectNameException
   {
      return pName == null ? OBJECT_NAME : pName;
   }

   /**
    * Creates the requested Timer if not already available
    * and start all added Schedules.
    * ATTENTION: the start of the schedules is not necessary when
    * the service is started but this method is also called when
    * the service is restarted and therefore schedules can be
    * available.
    */
   protected void startService() throws Exception
   {
	  mTimerObjectName = new ObjectName(mTimerName);
      if (!getServer().isRegistered(mTimerObjectName))
      {
         getServer().createMBean(Timer.class.getName(), mTimerObjectName);
      }
	  mTimer = (TimerMBean)MBeanServerInvocationHandler.newProxyInstance(getServer(),
			mTimerObjectName, TimerMBean.class, true);
	  mTimerEmitter = (NotificationEmitter)mTimer;
      if (!mTimer.isActive())
      {
		 mTimer.start();
      }
      startSchedules();
   }

   /**
    * Stops all Schedules.
    */
   protected void stopService()
   {
      stopSchedules(true);
   }

   /**
    * When Service is destroyed it will call the "unregisterProvider()"
    * on all register Providers to let them remove their Schedules and
    * being notified that they should stop providing.
    */
   protected void destroyService()
   {
      // Unregister all providers
      Iterator i = mSchedules.values().iterator();
      while (i.hasNext())
      {
         ScheduleInstance lInstance = (ScheduleInstance) i.next();
         unregisterProvider(lInstance.mProvider.toString());
      }
   }
   
   // -------------------------------------------------------------------------
   // Inner Classes
   // -------------------------------------------------------------------------

   /**
    * This listener is waiting for its Timer Notification and call the
    * appropriate method on the given Target (MBean) and count down the
    * number of remaining repetitions.
    */
   public class MBeanListener implements NotificationListener
   {
      private final Logger log = Logger.getLogger(MBeanListener.class);

      private ScheduleInstance mSchedule;

      public MBeanListener(ScheduleInstance pSchedule)
      {
         mSchedule = pSchedule;
      }

      public void handleNotification(Notification pNotification, Object pHandback)
      {
		 boolean trace = log.isTraceEnabled();
		 if (trace) {
			log.trace("MBeanListener.handleNotification: " + pNotification);
		 }

		 try
		 {
			if (!isStarted()) {
			   log.trace("Scheduler not started");
			   mSchedule.stop();
			   return;
			}
			if (mSchedule.mRemainingRepetitions == 0)
			{
			   log.trace("No more repetitions");
			   mSchedule.stop();
			   return;
			}
			if (mIsPaused.get())
			{
			   log.trace("Paused");
			   return;
			}
			if (mSchedule.mRemainingRepetitions > 0)
			{
			   mSchedule.mRemainingRepetitions--;
			   if (trace)
				  log.trace("Remaining repetitions: " + mSchedule.mRemainingRepetitions);
			}
			Object[] lArguments = getArguments(pNotification);
			if (trace)
			{
			   log.trace("invoke " + mSchedule);
			   log.trace("arguments are: " + Arrays.asList(lArguments));
			}
			
			ObjectName on = mSchedule.mTarget;
			String mn = mSchedule.mMethodName;
			getServer().invoke(on, mn, lArguments,
					mSchedule.mSchedulableMBeanArgumentTypes
			);
		 }
		 catch (Exception e)
		 {
			log.error("Invoke failed: " + mSchedule.getTargetString(), e);
		 }
      }

	  private Object[] getArguments(Notification pNotification)
	  {
		 Object[] lArguments = new Object[mSchedule.mSchedulableMBeanArguments.length];
		 Date lTimeStamp = new Date(pNotification.getTimeStamp());
		 for (int i = 0; i < lArguments.length; i++)
		 {
			switch (mSchedule.mSchedulableMBeanArguments[i])
			{
			   case ID:
				  lArguments[i] = pNotification.getUserData();
				  break;
			   case NOTIFICATION:
				  lArguments[i] = pNotification;
				  break;
			   case DATE:
				  lArguments[i] = lTimeStamp;
				  break;
			   case REPETITIONS:
				  lArguments[i] = new Long(mSchedule.mRemainingRepetitions);
				  break;
			   case SCHEDULER_NAME:
				  lArguments[i] = getServiceName();
				  break;
			   case NEXT_DATE:
				  lArguments[i] = new Date(lTimeStamp.getTime() + mSchedule.mPeriod);
				  break;
			   default:
				  lArguments[i] = null;
			}
		 }
		 return lArguments;
	  }

   }

   /**
    * Filter to ensure that each Scheduler only gets notified when it is supposed to.
    */
   static class IdNotificationFilter implements javax.management.NotificationFilter
   {
      private static final Logger log = Logger.getLogger(IdNotificationFilter.class);

      private Integer filterId;

      /**
       * Create a Filter.
       * @param pId the Scheduler id
       */
      public IdNotificationFilter(int filterId)
      {
         this.filterId = new Integer(filterId);
      }

      /**
       * Determine if the notification should be sent to this Scheduler
       */
      public boolean isNotificationEnabled(Notification pNotification)
      {
         if (!(pNotification instanceof TimerNotification))
			return false;
		 TimerNotification lTimerNotification = (TimerNotification) pNotification;
		 if (log.isTraceEnabled())
			log.trace("isNotificationEnabled(), filterId=" + filterId +
			   ", notification=" + pNotification +
			   ", notificationId=" + lTimerNotification.getNotificationID() +
			   ", timestamp=" + lTimerNotification.getTimeStamp() +
			   ", message=" + lTimerNotification.getMessage()
			);
		 return lTimerNotification.getNotificationID().equals(filterId);
      }
   }

   /**
    * Represents a single Schedule which can be started and stopped
    * if necessary.
    */
   private class ScheduleInstance
   {

      private final Logger log = Logger.getLogger(ScheduleInstance.class);
      private int mIdentification;
      private MBeanListener mListener;

      public int mNotificationID;
      public ObjectName mProvider;
      public ObjectName mTarget;
      public int mInitialRepetitions;
      public int mRemainingRepetitions = 0;
      public Date mStartDate;
      public long mPeriod;
      public String mMethodName;
      public int[] mSchedulableMBeanArguments;
      public String[] mSchedulableMBeanArgumentTypes;

      public ScheduleInstance(
              ObjectName pProvider,
              ObjectName pTarget,
              String pMethodName,
              String[] pMethodArguments,
              Date pStartDate,
              int pRepetitions,
              long pPeriod
              )
      {
         mProvider = pProvider;
         mTarget = pTarget;
         mInitialRepetitions = pRepetitions;
         mStartDate = pStartDate;
         mPeriod = pPeriod;
         mMethodName = pMethodName;
         mSchedulableMBeanArguments = new int[pMethodArguments.length];
         mSchedulableMBeanArgumentTypes = new String[pMethodArguments.length];
         for (int i = 0; i < pMethodArguments.length; i++)
         {
            String lToken = pMethodArguments[i];
            if (lToken.equals("ID"))
            {
               mSchedulableMBeanArguments[i] = ID;
               mSchedulableMBeanArgumentTypes[i] = Integer.class.getName();
            }
            else if (lToken.equals("NOTIFICATION"))
            {
               mSchedulableMBeanArguments[i] = NOTIFICATION;
               mSchedulableMBeanArgumentTypes[i] = Notification.class.getName();
            }
            else if (lToken.equals("NEXT_DATE"))
            {
               mSchedulableMBeanArguments[i] = NEXT_DATE;
               mSchedulableMBeanArgumentTypes[i] = Date.class.getName();
            }
            else if (lToken.equals("DATE"))
            {
               mSchedulableMBeanArguments[i] = DATE;
               mSchedulableMBeanArgumentTypes[i] = Date.class.getName();
            }
            else if (lToken.equals("REPETITIONS"))
            {
               mSchedulableMBeanArguments[i] = REPETITIONS;
               mSchedulableMBeanArgumentTypes[i] = Long.TYPE.getName();
            }
            else if (lToken.equals("SCHEDULER_NAME"))
            {
               mSchedulableMBeanArguments[i] = SCHEDULER_NAME;
               mSchedulableMBeanArgumentTypes[i] = ObjectName.class.getName();
            }
            else
            {
               mSchedulableMBeanArguments[i] = NULL;
               //AS ToDo: maybe later to check if this class exists !
               mSchedulableMBeanArgumentTypes[i] = lToken;
            }
         }
         mIdentification = sCounter.increment();
      }

      /**
       * Starts the Schedule by adding itself to the timer
       * and registering its listener to get the notifications
       * and hand over to the target
       **/
      public void start() throws JMException
      {
         Date lStartDate = null;
         // Check if initial start date is in the past
         if (mStartDate.getTime() < new Date().getTime() && mPeriod > 0)
         {
            // If then first check if a repetition is in the future
            long lNow = new Date().getTime() + 100;
            int lSkipRepeats = (int) ((lNow - mStartDate.getTime()) / mPeriod) + 1;
            log.debug("Old start date: " + mStartDate + ", now: " + new Date(lNow) + ", Skip repeats: " + lSkipRepeats);
            if (mInitialRepetitions > 0)
            {
               // If not infinit loop
               if (lSkipRepeats >= mInitialRepetitions)
               {
                  // No repetition left -> exit
                  log.warn("No repetitions left because start date is in the past and could " +
                          "not be reached by Initial Repetitions * Schedule Period");
                  return;
               }
               else
               {
                  // Reduce the missed hits
                  mRemainingRepetitions = mInitialRepetitions - lSkipRepeats;
               }
            }
            else
            {
               if (mInitialRepetitions == 0)
               {
                  mRemainingRepetitions = 0;
               }
               else
               {
                  mRemainingRepetitions = -1;
               }
            }
            lStartDate = new Date(mStartDate.getTime() + (lSkipRepeats * mPeriod));
         }
         else
         {
            lStartDate = mStartDate;
            mRemainingRepetitions = mInitialRepetitions;
         }
         mNotificationID = mTimer.addNotification(
			   "Schedule", "Scheduler Notification",
			   new Integer(getID()), // User Object
			   lStartDate,
			   new Long(mPeriod),
			   mRemainingRepetitions < 0 ? new Long(0) : new Long(mRemainingRepetitions),
			   Boolean.valueOf(mFixedRate)
		 );
         mListener = new MBeanListener(this);
		 mTimerEmitter.addNotificationListener(
			   mListener,
               new IdNotificationFilter(mNotificationID),
               // No object handback necessary
               null
         );
         log.debug("start(), add Notification to Timer with ID: " + mNotificationID);
      }

      /**
       * Stops the Schedule by remove itself from the timer
       * and removing the listener
       **/
      public void stop()
              throws JMException
      {
         log.debug("stopSchedule(), notification id: " + mNotificationID);
		 mTimerEmitter.removeNotificationListener(mListener);
		 try
		 {
			mTimer.removeNotification(mNotificationID);
		 }
		 catch (InstanceNotFoundException e)
		 {
		     log.trace(e);
		 }
      }

      public int getID()
      {
         return mIdentification;
      }

	  public String toString()
	  {
		 return "Schedule target=" + getTargetString();
	  }

	  public String getTargetString()
	  {
		 return mTarget + " " + mMethodName + "" + Arrays.asList(mSchedulableMBeanArgumentTypes);
	  }

   }
}
