package org.jboss.seam.async;

import static org.jboss.seam.annotations.Install.BUILT_IN;

import java.io.InputStream;
import java.rmi.server.UID;
import java.text.ParseException;
import java.util.Date;

import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.intercept.InvocationContext;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;

/**
 * Dispatcher implementation that uses the Quartz library.
 * 
 * @author Michael Yuan
 *
 */
@Startup
@Scope(ScopeType.APPLICATION)
@Name("org.jboss.seam.async.dispatcher")
@Install(value=false, precedence=BUILT_IN)
@BypassInterceptors
public class QuartzDispatcher extends AbstractDispatcher<QuartzTriggerHandle, Schedule>
{
   
   private static final LogProvider log = Logging.getLogProvider(QuartzDispatcher.class);
   
   private Scheduler scheduler;

   @Create
   public void initScheduler() throws SchedulerException
   {
       StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();

       //TODO: magical properties files are *not* the way to config Seam apps!
       InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("seam.quartz.properties");
       if (is != null)
       {
         schedulerFactory.initialize(is);
         log.debug("Found seam.quartz.properties file. Using it for Quartz config.");
       } 
       else 
       {
         schedulerFactory.initialize();
         log.warn("No seam.quartz.properties file. Using in-memory job store.");
       }
    
       scheduler = schedulerFactory.getScheduler();
       scheduler.start();
   }

   public QuartzTriggerHandle scheduleAsynchronousEvent(String type, Object... parameters)
   {  
      String jobName = nextUniqueName();
      String triggerName = nextUniqueName();
      
      JobDetail jobDetail = new JobDetail(jobName, null, QuartzJob.class);
      jobDetail.getJobDataMap().put("async", new AsynchronousEvent(type, parameters));
       
      SimpleTrigger trigger = new SimpleTrigger(triggerName, null);      
      try 
      {
        scheduler.scheduleJob(jobDetail, trigger);
        return new QuartzTriggerHandle(triggerName);
      } 
      catch (Exception se) 
      {
        log.debug("Cannot Schedule a Quartz Job");
        throw new RuntimeException(se);
      }
   }
    
   public QuartzTriggerHandle scheduleTimedEvent(String type, Schedule schedule, Object... parameters)
   {
      return scheduleWithQuartzServiceAndWrapExceptions( schedule, new AsynchronousEvent(type, parameters) );
   }
   
   public QuartzTriggerHandle scheduleInvocation(InvocationContext invocation, Component component)
   {
      return scheduleWithQuartzServiceAndWrapExceptions( createSchedule(invocation), new AsynchronousInvocation(invocation, component) );
   }

   private static Date calculateDelayedDate (long delay)
   {
      Date now = new Date ();
      now.setTime(now.getTime() + delay);
      return now;
   }
   
   private QuartzTriggerHandle scheduleWithQuartzServiceAndWrapExceptions(Schedule schedule, Asynchronous async)
   {
       try
       {
           return scheduleWithQuartzService(schedule, async);
       }
       catch (ParseException pe)
       {
           throw new RuntimeException(pe);
       }
       catch (SchedulerException se)
       {
           throw new RuntimeException(se);
       }
   }

   private QuartzTriggerHandle scheduleWithQuartzService(Schedule schedule, Asynchronous async) throws SchedulerException, ParseException
   {
      String jobName = nextUniqueName();
      String triggerName = nextUniqueName();
      
      JobDetail jobDetail = new JobDetail(jobName, null, QuartzJob.class);
      jobDetail.getJobDataMap().put("async", async);

      if (schedule instanceof CronSchedule) 
      {
          CronSchedule cronSchedule = (CronSchedule) schedule; 
          CronTrigger trigger = new CronTrigger (triggerName, null);
          trigger.setCronExpression(cronSchedule.getCron());
          trigger.setEndTime(cronSchedule.getFinalExpiration());
        
          if ( cronSchedule.getExpiration()!=null )
          {
            trigger.setStartTime (cronSchedule.getExpiration());
          }
          else if ( cronSchedule.getDuration()!=null )
          {
            trigger.setStartTime (calculateDelayedDate(cronSchedule.getDuration()));
          }
        
          scheduler.scheduleJob( jobDetail, trigger );
      }
      else if (schedule instanceof TimerSchedule)
      {
          TimerSchedule timerSchedule = (TimerSchedule) schedule;
          if (timerSchedule.getIntervalDuration() != null) 
          {
             if ( timerSchedule.getExpiration()!=null )
             {
                SimpleTrigger trigger = new SimpleTrigger(triggerName, null, 
                        timerSchedule.getExpiration(), 
                        timerSchedule.getFinalExpiration(), 
                        SimpleTrigger.REPEAT_INDEFINITELY, 
                        timerSchedule.getIntervalDuration());
                scheduler.scheduleJob( jobDetail, trigger );
    
             }
             else if ( timerSchedule.getDuration()!=null )
             {
                 SimpleTrigger trigger = new SimpleTrigger(triggerName, null, 
                         calculateDelayedDate(timerSchedule.getDuration()), 
                         timerSchedule.getFinalExpiration(), SimpleTrigger.REPEAT_INDEFINITELY, 
                         timerSchedule.getIntervalDuration());
                 scheduler.scheduleJob( jobDetail, trigger );
    
             }
             else
             {
                SimpleTrigger trigger = new SimpleTrigger(triggerName, null, new Date(), 
                        timerSchedule.getFinalExpiration(), 
                        SimpleTrigger.REPEAT_INDEFINITELY, 
                        timerSchedule.getIntervalDuration());
                scheduler.scheduleJob( jobDetail, trigger );
    
             }
          } 
          else 
          {
            if ( schedule.getExpiration()!=null )
            {
                SimpleTrigger trigger = new SimpleTrigger (triggerName, null, schedule.getExpiration());
                scheduler.scheduleJob(jobDetail, trigger);
    
            }
            else if ( schedule.getDuration()!=null )
            {
                SimpleTrigger trigger = new SimpleTrigger (triggerName, null, 
                        calculateDelayedDate(schedule.getDuration()));
                scheduler.scheduleJob(jobDetail, trigger);
    
            }
            else
            {
               SimpleTrigger trigger = new SimpleTrigger(triggerName, null);
               scheduler.scheduleJob(jobDetail, trigger);
    
            }
          }
      }
      else
      {
          throw new IllegalArgumentException("unrecognized schedule type");
      }

      return new QuartzTriggerHandle(triggerName);
   }
   
   private String nextUniqueName ()
   {
      return (new UID()).toString();
   }
   
   @Destroy
   public void destroy() throws SchedulerException
   {
      scheduler.shutdown();
   }
   
   public static class QuartzJob implements Job
   {
      private Asynchronous async;
      
      public QuartzJob() { }

      public void execute(JobExecutionContext context)
          throws JobExecutionException
      {
         JobDataMap dataMap = context.getJobDetail().getJobDataMap();
         async = (Asynchronous)dataMap.get("async");
         QuartzTriggerHandle handle = new QuartzTriggerHandle(context.getTrigger().getName());
         try
         {
            async.execute(handle);
         }
         catch (Exception e) 
         {
            async.handleException(e, handle);
         }
      }
   }

   public Scheduler getScheduler()
   {
      return scheduler;
   }
   


   public static QuartzDispatcher instance()
   {
      return (QuartzDispatcher) AbstractDispatcher.instance();
   }

}
