/*****************************************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ****************************************************************************************/
// Package
package gov.va.med.fw.scheduling;

// Java classes
import java.io.Serializable;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.quartz.JobDetail;
import org.quartz.JobPersistenceException;
import org.quartz.ObjectAlreadyExistsException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.springframework.beans.factory.InitializingBean;

import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.service.trigger.TriggerEvent;
import gov.va.med.fw.util.InvalidConfigurationException;

/**
 * Provides methods to schedule, to unschedule, and to reschedule a job. This service
 * delegates to the underlying Quart Scheduling framework to do the actual scheduling
 * task. This service is aimed to encapsulate the actual implementation and ocnfiguration
 * of a scheduler and to provide the most convenient methods for scheduling a task. For
 * more advance tasks, consider using a scheduler that can be obtained through a
 * getScheduler method. <p/> Project: Framework</br> Created on: 10:52:58 AM </br>
 * 
 * @author DNS   LEV
 */
public class QuartzSchedulingService extends AbstractComponent 
   implements Serializable, SchedulingService {
   
   /**
    * An instance of serialVersionUID
    */
   private static final long serialVersionUID = 3171280925841379597L;

   /**
    * An instance of TRIGGER_NAME_MAX_LENGTH
    */
   private static int TRIGGER_NAME_MAX_LENGTH = 80;

   /**
    * An instance of scheduler
    */
   private Scheduler scheduler = null;

   private String immediateTriggerKey;

   private String futureTriggerKey;

   private boolean isClustered = false;

   /**
    * A default constructor
    */
   public QuartzSchedulingService() {
      super();
   }

   /**
    * @see gov.va.med.fw.service.AbstractComponent#afterPropertiesSet()
    */
   public void afterPropertiesSet() throws Exception {
      super.afterPropertiesSet();
      Validate.notNull(this.scheduler, "A scheduler must be configured");
      Validate.notNull(this.immediateTriggerKey,
            "A immediateTriggerKey must be configured");
      Validate.notNull(this.futureTriggerKey, "A futureTriggerKey must be configured");
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#isClustered()
    */
   public boolean isClustered() {
      return this.isClustered;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#setIsClustered(boolean)
    */
   public void setIsClustered(boolean flag) {
      this.isClustered = flag;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getScheduler()
    */
   public Scheduler getScheduler() {
      return scheduler;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#setScheduler(org.quartz.Scheduler)
    */
   public void setScheduler(Scheduler scheduler) {
      this.scheduler = scheduler;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getFutureTriggerKey()
    */
   public String getFutureTriggerKey() {
      return futureTriggerKey;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#setFutureTriggerKey(java.lang.String)
    */
   public void setFutureTriggerKey(String futureTriggerKey) {
      this.futureTriggerKey = futureTriggerKey;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getImmediateTriggerKey()
    */
   public String getImmediateTriggerKey() {
      return immediateTriggerKey;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#setImmediateTriggerKey(java.lang.String)
    */
   public void setImmediateTriggerKey(String immediateTriggerKey) {
      this.immediateTriggerKey = immediateTriggerKey;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#schedule(org.quartz.JobDetail,
    *      org.quartz.Trigger)
    */
   public Date schedule(JobDetail jobDetail, Trigger trigger) throws SchedulerException {
      return scheduleJob(jobDetail, trigger);
   }

   private Date scheduleJob(Trigger trigger) throws SchedulerException {
      return scheduleJob(null, trigger);
   }

   /**
    * @param job
    *           the job
    * @param trigger
    *           the trigger
    * @return the date scheduled
    * @throws SchedulerException
    *            if a problem was encountered
    */
   private Date scheduleJob(JobDetail job, Trigger trigger) throws SchedulerException {

      boolean ok = false;
      Date date = null;
      int i = 1;
      String baseTriggerName = trigger.getName();
      
      // third method for uniqueness across trigger names (other two are in
      // getSimpleTrigger)
      // CCR 10281. If there is a trigger alredy with the same job name we are getting in to infinete loop. quartz code looks for uniqueness in the job
      // Name and the code here is trying make the trigger unique. So it was never able get the uniqueness. If there is a trigger for a person in the 
      // waiting state there is no need to schedule a new one. So removing the while loop that tries to adjust the triggername by appending a number to it
      // If we need to do it , we should adjust the jobname NOT trigger name
      //while( !ok ) {
         try {
            // since trigger name is adjusted, confirm its length is "managed" to its last
            // n characters
            if( trigger.getName().length() > TRIGGER_NAME_MAX_LENGTH ) {
               trigger.setName(StringUtils.substring(trigger.getName(), trigger.getName()
                     .length()
                     - TRIGGER_NAME_MAX_LENGTH, trigger.getName().length()));
            }

            if( job != null )
               date = getScheduler().scheduleJob(job, trigger);
            else
               date = getScheduler().scheduleJob(trigger);
            
            ok = true;
         }
         catch( JobPersistenceException e ) {
             if( logger.isErrorEnabled() ) {
                 logger.error("Error while Scheduling Trigger["  + " TriggerName["+  trigger.getName() + "]" + " in group [" + trigger.getGroup()+"]}",e); 
              }
           
            if (e instanceof ObjectAlreadyExistsException )
            {
                if( logger.isErrorEnabled() ) {
                    logger.error("Error while Scheduling Trigger["  + " TriggerName["+  trigger.getName() + "]" + " in group ["
                          + trigger.getGroup() + 
                    "]  But failed with Object Already Exists Exception, NO Need to create the trigger!");
                 }
                //i++;
                //trigger.setName(baseTriggerName + "-" + i);
            }
            else
            {
                // rethrowing the error 
                throw e;
            }
            
         }
      //}
      return date;
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#schedule(org.quartz.JobDetail,
    *      java.util.List, double, java.lang.String, java.lang.String)
    */
   public Date schedule(JobDetail jobDetail, List contextDataForThisTrigger,
         double numberOfHoursDelayed, String triggerName, String triggerGroup)
         throws SchedulerException {

      // Extract job info froma job detail
      String jobName = jobDetail.getName();
      String jobGroup = jobDetail.getGroup();

      // Init a trigger
      SimpleTrigger trigger = getSimpleTrigger(futureTriggerKey, jobDetail, triggerName,
            triggerGroup, false);
      trigger.setJobName(jobName);
      trigger.setJobGroup(jobGroup);
      trigger.setName(triggerName);
      trigger.setGroup(triggerGroup);

      long millis = ( (long)Math.round(numberOfHoursDelayed * 60) ) * 60 * 1000;
      trigger.setStartTime(new Date(System.currentTimeMillis() + millis));
      if( contextDataForThisTrigger != null && !contextDataForThisTrigger.isEmpty() ) {
         trigger.getJobDataMap().put(
               ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS,
               contextDataForThisTrigger.toArray());
      }
      else {
         // to be safe...
         trigger.getJobDataMap().remove(
               ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS);
      }
      return schedule(jobDetail, trigger);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#schedule(org.quartz.Trigger)
    */
   public Date schedule(Trigger trigger) throws SchedulerException {
      return scheduleJob(trigger);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#triggerImmediately(gov.va.med.fw.scheduling.ScheduledProcessInvocationContext)
    */
   public void fireJobWithImmediateTrigger(ScheduledProcessInvocationContext invocationContext)
         throws SchedulerException {

      if( logger.isInfoEnabled() ) {
         logger.info("Scheduling job[" + invocationContext.getJobName() + "]"
               + " in group [" + invocationContext.getJobGroup()
               + "] for immediate execution");
      }
      /*
       * this is not as flexible since it requires the job to be known and registered with
       * scheduler scheduler.triggerJob(invocationContext.getJobName(),
       * invocationContext.getJobGroup());
       */
      schedule(getSimpleTrigger(immediateTriggerKey, invocationContext));
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#triggerImmediately(org.quartz.JobDetail)
    */
   public void fireJobWithImmediateTrigger(JobDetail job) throws SchedulerException {

      Validate.notNull(job, "A job must not be null");
      if( logger.isInfoEnabled() ) {
         logger.info("Scheduling job[" + job.getName() + "]" + " in group ["
               + job.getGroup() + "] for immediate execution");
      }
      this.schedule(this
            .getSimpleTrigger(this.immediateTriggerKey, job, null, null, true));
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#triggerImmediately(gov.va.med.fw.service.trigger.TriggerEvent)
    */
   public void fireJobWithImmediateTrigger(TriggerEvent event) throws SchedulerException {
	   fireJobWithImmediateTrigger((ScheduledProcessInvocationContext)event.getPayload());
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#unschedule(java.lang.String,
    *      java.lang.String)
    */
   public boolean unschedule(String triggerName, String groupName)
         throws SchedulerException {
      Trigger trigger = getTrigger(triggerName, groupName);
      return ( ( trigger != null ) && ( this.getScheduler().unscheduleJob(triggerName,
            groupName) ) );
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#reschedule(java.lang.String,
    *      java.lang.String, org.quartz.Trigger)
    */
   public Date reschedule(String triggerName, String groupName, Trigger newTrigger)
         throws SchedulerException {
      return this.getScheduler().rescheduleJob(triggerName, groupName, newTrigger);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#interrupt(java.lang.String,
    *      java.lang.String)
    */
   public boolean interrupt(String jobName, String groupName) throws SchedulerException {
      Validate.notNull(jobName, "JobName must not be null.");
      if( logger.isInfoEnabled() ) {
         logger.info("Interrupting a job[" + jobName + "]" + " in group [" + groupName
               + "] .");
      }
      return getScheduler().interrupt(jobName, groupName);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#interrupt(org.quartz.JobDetail)
    */
   public boolean interrupt(JobDetail jobDetail) throws SchedulerException {
      Validate.notNull(jobDetail, "JobDetail must not be null.");
      return interrupt(jobDetail.getName(), jobDetail.getGroup());
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getJobGroupNames()
    */
   public String[] getJobGroupNames() throws SchedulerException {
      return scheduler.getJobGroupNames();
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getJobNames(java.lang.String)
    */
   public String[] getJobNames(String groupName) throws SchedulerException {
      return scheduler.getJobNames(groupName);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getTriggerGroupNames()
    */
   public String[] getTriggerGroupNames() throws SchedulerException {
      return scheduler.getTriggerGroupNames();
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getTriggerNames(java.lang.String)
    */
   public String[] getTriggerNames(String groupName) throws SchedulerException {
      return scheduler.getTriggerNames(groupName);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getJobDetail(java.lang.String,
    *      java.lang.String)
    */
   public JobDetail getJobDetail(String jobName, String jobGroup)
         throws SchedulerException {
      return scheduler.getJobDetail(jobName, jobGroup);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getTrigger(java.lang.String,
    *      java.lang.String)
    */
   public Trigger getTrigger(String triggerName, String triggerGroup)
         throws SchedulerException {
      return scheduler.getTrigger(triggerName, triggerGroup);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#getTriggerStatus(java.lang.String,
    *      java.lang.String)
    */
   public TriggerStatus getTriggerStatus(String triggerName, String triggerGroup)
         throws SchedulerException {
      Trigger trigger = getTrigger(triggerName, triggerGroup);
      TriggerStatus status = new TriggerStatus();
      if( trigger != null ) {
         status.setNextFireTime(trigger.getNextFireTime());
         int quartzStatus = scheduler.getTriggerState(triggerName, triggerGroup);
         if( quartzStatus == Trigger.STATE_NORMAL )
            status.setStatus(TriggerStatus.NORMAL_STATUS);
         else if( quartzStatus == Trigger.STATE_ERROR )
            status.setStatus("ERROR");
         else if( quartzStatus == Trigger.STATE_COMPLETE )
            status.setStatus("COMPLETE");
         else if( quartzStatus == Trigger.STATE_PAUSED )
            status.setStatus(TriggerStatus.PAUSED_STATUS);
         else if( quartzStatus == Trigger.STATE_BLOCKED )
            status.setStatus("BLOCKED");
         else
            status.setStatus(StringUtils.EMPTY + quartzStatus);
      }
      return status;
   }

   /**
    * Returns a simple trigger defined in a spring context by the specific key. A trigger
    * is initialized with a job name, a group name, and a job defined in a spring
    * appplication context by a job name
    * 
    * @param triggerKey
    *           A key to look up for a trigger in an application context
    * @param context
    *           the scheduled process invocation context
    * @return the simple trigger
    * @throws SchedulerException
    *            if a problem was encountered
    */
   private SimpleTrigger getSimpleTrigger(String triggerKey,
         ScheduledProcessInvocationContext context) throws SchedulerException {
      try {
         Object component = getComponent(context.getJobDetailBeanName(), JobDetail.class);
         JobDetail job = component instanceof JobDetail ? (JobDetail)component : null;

         if (job != null)  {
	         job.setName(context.getJobName());
	         job.setGroup(context.getJobGroup());
         }

         SimpleTrigger trigger = getSimpleTrigger(triggerKey, job, null, null, true);

         // set dynamic stuff on Trigger (not Job)...this allows to reuse same
         // "core" Job for static and dynamic use
         if( context.getInvocationArguments() != null ) {
            trigger.getJobDataMap().put(
                  ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS,
                  context.getInvocationArguments());
         }
         else {
            // to be safe...
            trigger.getJobDataMap().remove(
                  ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS);
         }
         // ScheduledProcessInvocationContext.EXECUTION_CONTEXT should always be there
         trigger.getJobDataMap().put(ScheduledProcessInvocationContext.EXECUTION_CONTEXT,
               context.getExecutionContext());

         return trigger;
      }
      catch( Throwable t ) {
         InvalidConfigurationException e = new InvalidConfigurationException(
               "Unable to get a SimpleTrigger for job", t);
         throw new SchedulerException("Unable to trigger task immediately", e);
      }
   }

   /**
    * Returns a simple trigger defined in a spring context by the specific key. A trigger
    * is initialized with a job name, a group name, and the specific job
    * 
    * @param triggerKey
    *           the trigger key
    * @param job
    *           the job detail information
    * @param triggerName
    *           the trigger name
    * @param triggerGroup
    *           the group name
    * @return the simple trigger
    * @throws SchedulerException
    *            if a problem was encountered
    */
   private SimpleTrigger getSimpleTrigger(String triggerKey, JobDetail job,
         String triggerName, String triggerGroup, boolean addJob)
         throws SchedulerException {
      try {
         // Get a simple trigger defined in a current context
         SimpleTrigger newTrigger = (SimpleTrigger)this.getComponent(triggerKey,
               SimpleTrigger.class);

         // first method of ensuring uniqueness for a reused SimpleTrigger
         String baseName = null;
         if( triggerName != null ) {
            baseName = triggerName;
         }
         else {
            baseName = newTrigger.getName() + "-" + job.getName();
         }
         newTrigger.setName(baseName + "-" + System.currentTimeMillis());
         if( triggerGroup != null )
            newTrigger.setGroup(triggerGroup);

         // second method of ensuring uniqueness for a reused SimpleTrigger
         // (check to see if there)
         Trigger existingTrigger = this.scheduler.getTrigger(newTrigger.getName(),
               newTrigger.getGroup());

         while( existingTrigger != null ) {
            newTrigger.setName(baseName + "-" + System.currentTimeMillis());
            existingTrigger = this.scheduler.getTrigger(newTrigger.getName(), newTrigger
                  .getGroup());
         }

         newTrigger.setJobName(job.getName());
         newTrigger.setJobGroup(job.getGroup());

         if( newTrigger instanceof InitializingBean ) {
            // force defaulting on the trigger ???
            ( (InitializingBean)newTrigger ).afterPropertiesSet();
         }
         // for those jobs that are not already registered with Scheduler, do
         // this
         if( addJob )
            scheduler.addJob(job, true);
         return newTrigger;
      }
      catch( Exception e ) {
         InvalidConfigurationException e2 = new InvalidConfigurationException(
               "Unable to get a SimpleTrigger for job", e);
         throw new SchedulerException("Unable to trigger task immediately", e2);
      }
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#pauseJob(java.lang.String,
    *      java.lang.String)
    */
   public void pauseJob(String jobName, String jobGroup) throws SchedulerException {
      scheduler.pauseJob(jobName, jobGroup);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#pauseAllJobs()
    */
   public void pauseAllJobs() throws SchedulerException {
      scheduler.pauseAll();
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#resumeJob(java.lang.String,
    *      java.lang.String)
    */
   public void resumeJob(String jobName, String jobGroup) throws SchedulerException {
      scheduler.resumeJob(jobName, jobGroup);
   }

   /**
    * @see gov.va.med.fw.scheduling.SchedulingService#resumeAllJobs()
    */
   public void resumeAllJobs() throws SchedulerException {
      scheduler.resumeAll();
   }

   public void pauseTriggerGroup(String triggerGroup) throws SchedulerException {
	scheduler.pauseTriggerGroup(triggerGroup);	
   }
	
   public void resumeTriggerGroup(String triggerGroup) throws SchedulerException {
	   scheduler.resumeTriggerGroup(triggerGroup);
	}
}