/**
 * QuartzScheduleManager.java
 * Created on May 15, 2008
 * (C) Copyright TANDBERG Television Ltd.
 */
package com.tandbergtv.watchpoint.pmm.job.scheduling;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import com.tandbergtv.watchpoint.pmm.entities.JobParameter;
import com.tandbergtv.watchpoint.pmm.entities.RuleParameter;
import com.tandbergtv.watchpoint.pmm.job.timers.ITimeRepresentation;
import com.tandbergtv.watchpoint.pmm.job.timers.TimerResult;
import com.tandbergtv.watchpoint.pmm.job.util.JobScheduleInfoConstants;

/**
 * This class is a wrapper for all communication with Quartz.
 * 
 * @author spuranik
 */
public class QuartzScheduleManager implements IJobScheduleManager {

	private static IJobScheduleManager instance;
	private Scheduler scheduler;

	private static String SERVICE_NAME = "Job Scheduling service";
	public static String JOB_GROUP_NAME = "Workflow PMM Jobs";
	private static String TRIGGER_PREFIX = "TRIGGER";
	private static String TRIGGER_JOBID_DELIMETER = "_";
	private static int THREAD_FACTORY_THREAD_COUNT = 1;
	private static String JOB_PROPERTIES_FILENAME = "com/tandbergtv/watchpoint/pmm/job/scheduling/job.quartz.properties";

	private static final Logger logger = Logger.getLogger(QuartzScheduleManager.class);

	public static synchronized IJobScheduleManager getInstance() {
		if (instance == null) {
			instance = new QuartzScheduleManager();
		}
		return instance;
	}

	/**
	 * @return the scheduler
	 */
	public Scheduler getScheduler() {
		return scheduler;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.watchpoint.pmm.job.scheduling.IJobScheduleManager#addSchedule(com.tandbergtv.watchpoint.pmm.entities.Job)
	 */
	@SuppressWarnings("unchecked")
	public synchronized void addSchedule(com.tandbergtv.watchpoint.pmm.entities.Job job) {
		// create job detail, job datamap, trigger and schedule the job
		// use job id as the name so the user has the flexibility of changing
		// the name in job update.
		String jobName = Long.toString(job.getId());
		Date jobEndDate = job.getRule().getEndDate();

		try {
			Class<Job> callbackClass = (Class<Job>) Class.forName(job.getRule().getCallbackClass());

			// all jobs are added to the same group
			JobDetail jobDetail = new JobDetail(jobName, JOB_GROUP_NAME, callbackClass);

			jobDetail.setDurability(true);
			jobDetail.setRequestsRecovery(true);
			jobDetail.setVolatility(true);

			jobDetail.setJobDataMap(buildJobDataMap(job));

			String triggerName = TRIGGER_PREFIX + TRIGGER_JOBID_DELIMETER + job.getId();

			Class<ITimeRepresentation> timerClass = (Class<ITimeRepresentation>) Class.forName(job
					.getRule().getTimeClass());
			ITimeRepresentation timer = timerClass.newInstance();

			// the startDate can be modified by the timer class
			// hence using a copy of it so it doesn't change in the job object.
			// This means the when the job is being edited, the start date
			// may not be the same as the one in Quartz. But why would the user
			// care as long as the timing is set right!
			TimerResult result = (TimerResult) timer.getTime(job.getRule().getType().getParams(),
					job.getRule().getParams(), job.getRule().getStartDate());

			// Don't proceed if the timer class is not found.
			if (result == null) {
				String errorMsg = "Error while getting time from timer class. Result is null.";
				throw new RuntimeException(errorMsg);
			}
			
			SimpleTrigger trigger;
			if (result.getIsPeriodic()) {
				logger.info("Setting a periodic job starting at: " + result.getStartDate()
						+ " every " + result.getRepeatInterval() + " msec.");
				trigger = new SimpleTrigger(triggerName, JOB_GROUP_NAME, jobName, JOB_GROUP_NAME,
						result.getStartDate(), jobEndDate, SimpleTrigger.REPEAT_INDEFINITELY,
						result.getRepeatInterval());
			} else {
				logger.info("Setting a one time job for date: " + result.getStartDate());
				trigger = new SimpleTrigger(triggerName, JOB_GROUP_NAME, result.getStartDate());
				trigger.setJobName(jobName);
				trigger.setJobGroup(JOB_GROUP_NAME);
			}
			trigger
					.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT);
			trigger.setVolatility(true);
			trigger.addTriggerListener(MissedTriggerListener.NAME);

			scheduler.addJob(jobDetail, true);
			scheduler.scheduleJob(trigger);

			logger.info("Done scheduling job: " + jobDetail.getName());

		} catch (SchedulerException se) {
			String errorMsg = "Error while scheduling job: " + se.toString();
			throw new RuntimeException(errorMsg, se);
		} catch (InstantiationException e1) {
			String errorMsg = "Error while instantiating timer class: " + e1.toString();
			throw new RuntimeException(errorMsg, e1);
		} catch (IllegalAccessException e2) {
			String errorMsg = "Error while accessing timer class: " + e2.toString();
			throw new RuntimeException(errorMsg, e2);
		} catch (ClassNotFoundException e3) {
			String errorMsg = "Error while loading timer class: " + e3.toString();
			throw new RuntimeException(errorMsg, e3);
		} catch (IllegalArgumentException e4) {
			String errorMsg = "Error while passing arguments for job/trigger: " + e4.toString();
			throw new RuntimeException(errorMsg, e4);			
		}		
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.watchpoint.pmm.job.scheduling.IJobScheduleManager#deleteSchedules(com.tandbergtv.watchpoint.pmm.entities.Job)
	 */
	public synchronized void deleteSchedules(com.tandbergtv.watchpoint.pmm.entities.Job job) {
		try {

			// delete the complete job that was added. We do this because
			// the callback info might have changed, not just the trigger time.
			String jobName = Long.toString(job.getId());

			if (scheduler.getJobDetail(jobName, JOB_GROUP_NAME) != null) {
				scheduler.deleteJob(jobName, JOB_GROUP_NAME);
			} else {
				String errorMsg = "Scheduler did not find job: " + jobName
						+ " hence cannot delete it.";
				throw new RuntimeException(errorMsg);
			}
		} catch (SchedulerException e) {
			String errorMsg = "Error while deleting job: " + job.getId() + ":" + e.toString();
			throw new RuntimeException(errorMsg, e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.watchpoint.pmm.job.scheduling.IJobScheduleManager#updateSchedule(com.tandbergtv.watchpoint.pmm.entities.Job)
	 */
	public synchronized void updateSchedule(com.tandbergtv.watchpoint.pmm.entities.Job job) {
		String jobName = Long.toString(job.getId());
		JobDetail detail = null;
		Trigger trigger = null;

		try {
			detail = scheduler.getJobDetail(jobName, JOB_GROUP_NAME);
			Trigger[] triggers = scheduler.getTriggersOfJob(jobName, JOB_GROUP_NAME);
			// In our system, a job can have only one trigger.
			// Trigger should ideally not be null. But if its an expired job it
			// may be null.
			trigger = (triggers != null && triggers.length == 1) ? triggers[0] : null;
			if (detail != null) {
				deleteSchedules(job);
				addSchedule(job);
			} else {
				String errorMsg = "Job: " + jobName + " does not exist.";
				throw new RuntimeException(errorMsg);
			}
		} catch (Exception e) {
			try {
				// wow exception inside an exception!
				// check if that job was deleted. In that case have to add it back.
				if (scheduler.getJobDetail(jobName, JOB_GROUP_NAME) == null) {
					scheduler.addJob(detail, true);
					if (trigger != null) {
						scheduler.scheduleJob(trigger);						
					}
				}
				// throw back the error that the job could not be updated
				String errorMsg = "Error while updating job schedule: " + e.toString();
				throw new RuntimeException(errorMsg, e);
			} catch (SchedulerException e1) {
				// in this case cant really rollback, so forget it!
				String errorMsg = "Error while updating job schedule: " + e.toString();
				throw new RuntimeException(errorMsg, e);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	public String getServiceName() {
		return SERVICE_NAME;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	public void start() {
		try {
			SchedulerFactory sf = new StdSchedulerFactory(JOB_PROPERTIES_FILENAME);
			scheduler = sf.getScheduler();
			if (scheduler == null) {
				String errorMsg = "Could not get scheduler from quartz scheduler factory.";
				throw new RuntimeException(errorMsg);
			}

			// start the scheduler in a separate thread. it could be a time consuming
			// event when crash recovery is enabled.
			// the return value is not read by any class at this point.
			// the intention is to not to block on start up.
			logger.debug("Will start the scheduler");
			Callable<Scheduler> starterThread = new Callable<Scheduler>() {
				public Scheduler call() throws Exception {
					if (scheduler.getTriggerListener(MissedTriggerListener.NAME) == null) {
						MissedTriggerListener triggerListener = new MissedTriggerListener();
						scheduler.addTriggerListener(triggerListener);
					}
					scheduler.start();
					logger.debug("There you go. Started the Quartz standard scheduler.");
					return scheduler;
				}
			};
			ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
					THREAD_FACTORY_THREAD_COUNT);
			executor.schedule(starterThread, 0L, TimeUnit.MILLISECONDS);
		} catch (SchedulerException e) {
			String errorMsg = "Error while getting/starting scheduler: " + e.toString();
			throw new RuntimeException(errorMsg, e);
		} catch (Exception e) {
			throw new RuntimeException(e.toString(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	public void stop() {
		try {
			scheduler.shutdown(true);
		} catch (SchedulerException e) {
			String errorMsg = "Error while stopping scheduler: " + e.toString();
			throw new RuntimeException(errorMsg, e);
		}
	}

	/**
	 * @return list of all currently scheduled job names
	 */
	public String[] getAllScheduledJobNames() {
		try {
			return scheduler.getJobNames(JOB_GROUP_NAME);
		} catch (SchedulerException e) {
			logger.error("Error while getting scheduled job names: " + e.toString(), e);
		}
		return null;
	}

	/**
	 * @return number of jobs executed
	 */
	public int getExecutedJobCount() {
		try {
			return scheduler.getMetaData().numJobsExecuted();
		} catch (SchedulerException e) {
			logger.error("Error while getting executed job count: " + e.toString(), e);
		}
		return 0;
	}

	/**
	 * Builds a map with contextId, job params, priority, template name, rule name and rule
	 * parameters. These values will be used when its time to execute the job.
	 * 
	 * @param job
	 *            the complete job object for which this schedule is being added/modified
	 * @return a map with all the required info
	 */
	private JobDataMap buildJobDataMap(com.tandbergtv.watchpoint.pmm.entities.Job job) {

		JobDataMap jobDataMap = new JobDataMap();

		jobDataMap.put(JobScheduleInfoConstants.JOB_NAME, job.getName());
		jobDataMap.put(JobScheduleInfoConstants.CONTEXTID, job.getContext().getId());

		List<JobParameter> jobParams = cloneJobParams(job.getJobParams());
		jobDataMap.put(JobScheduleInfoConstants.JOB_PARAMETERS, jobParams);

		jobDataMap.put(JobScheduleInfoConstants.JOB_PRIORITY, job.getPriority().toString());
		jobDataMap.put(JobScheduleInfoConstants.JOB_SELECTED_TEMPLATE_NAME, job.getTemplateName());
		jobDataMap.put(JobScheduleInfoConstants.JOB_RULE_NAME, job.getRule().getType().getName());

		List<RuleParameter> ruleParams = cloneRuleParams(job.getRule().getParams());
		jobDataMap.put(JobScheduleInfoConstants.JOB_RULE_PARAMETERS, ruleParams);

		return jobDataMap;
	}

	/**
	 * @param params
	 * @return
	 */
	private List<RuleParameter> cloneRuleParams(List<RuleParameter> params) {
		List<RuleParameter> clonedParams = new ArrayList<RuleParameter>();
		for (RuleParameter p : params) {
			RuleParameter param = new RuleParameter();
			// dont set rule
			param.setId(p.getId());
			param.setOrder(p.getOrder());
			param.setValue(p.getValue());
			clonedParams.add(param);
		}
		return clonedParams;
	}

	/**
	 * @param jobParams
	 * @return
	 */
	private List<JobParameter> cloneJobParams(List<JobParameter> jobParams) {

		List<JobParameter> clonedParams = new ArrayList<JobParameter>();
		for (JobParameter p : jobParams) {
			JobParameter param = new JobParameter();
			// dont set the Job
			param.setId(p.getId());
			param.setName(p.getName());
			param.setValue(p.getValue());

			clonedParams.add(param);
		}
		return clonedParams;
	}
}
