/*
 * RHQ Management Platform
 * Copyright (C) 2005-2014 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.server.scheduler;

import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Calendar;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerContext;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.SchedulerMetaData;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.quartz.UnableToInterruptJobException;
import org.quartz.spi.JobFactory;

/**
 * Wraps a Quartz scheduler and provides some enhanced scheduler methods.
 */
public class EnhancedSchedulerImpl implements EnhancedScheduler {
    @SuppressWarnings("unused")
    private static final long serialVersionUID = 1L;
    private final Log log = LogFactory.getLog(EnhancedSchedulerImpl.class.getName());
    private final Scheduler scheduler;

    public EnhancedSchedulerImpl(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    /**
     * @see EnhancedScheduler#scheduleRepeatingJob(String, String, JobDataMap, Class, boolean, boolean, long, long)
     */
    public void scheduleRepeatingJob(String name, String groupName, JobDataMap jobData, Class<? extends Job> jobClass,
        boolean rescheduleIfExists, boolean isVolatile, long initialDelay, long interval) throws SchedulerException {

        // See if the job is already scheduled and if so,
        // either remove it so we can reschedule it or keep it (based on rescheduleIfExists).
        JobDetail existingJob = getExistingJob(name, groupName, rescheduleIfExists);
        if (existingJob != null) {
            if (rescheduleIfExists) {
                log.debug("Looks like repeating job [" + name + ':' + groupName
                    + "] is already scheduled - removing it so it can be rescheduled...");
                if (!this.scheduler.deleteJob(name, groupName)) {
                    throw new SchedulerException("Failed to delete repeating job [" + existingJob
                        + "] in order to reschedule it.");
                }
            } else {
                log.debug("Looks like repeating job [" + name + ':' + groupName
                    + "] is already scheduled - leaving the original job as-is.");
                return;
            }
        }

        JobDetail job = createJobDetail(name, groupName, jobClass, isVolatile, jobData);
        SimpleTrigger trigger = createSimpleTrigger(name, groupName, isVolatile, initialDelay, interval);
        Date next = this.scheduler.scheduleJob(job, trigger);

        log.info("Scheduled job [" + name + ':' + groupName + "] to fire next at [" + next + "] and repeat every ["
            + interval + "] milliseconds");

        return;
    }

    /**
     * @see EnhancedScheduler#scheduleCronJob(String, String, JobDataMap, Class, boolean, boolean, String)
     */
    public void scheduleCronJob(String name, String groupName, JobDataMap jobData, Class<? extends Job> jobClass,
        boolean rescheduleIfExists, boolean isVolatile, String cronString) throws SchedulerException {

        scheduleCronJob(name, groupName, jobData, jobClass, rescheduleIfExists, isVolatile, cronString, null);
    }

    private void scheduleCronJob(String name, String groupName, JobDataMap jobData, Class<? extends Job> jobClass,
        boolean rescheduleIfExists, boolean isVolatile, String cronString, Integer misfireInstruction)
        throws SchedulerException {

        // See if the job is already scheduled and if so,
        // either remove it so we can reschedule it or keep it (based on rescheduleIfExists).
        JobDetail existingJob = getExistingJob(name, groupName, rescheduleIfExists);
        if (existingJob != null) {
            if (rescheduleIfExists) {
                log.debug("Looks like cron job [" + name + ':' + groupName
                    + "] is already scheduled - removing it so it can be rescheduled...");
                deleteJob(name, groupName, existingJob);
            } else {
                log.debug("Looks like cron job [" + name + ':' + groupName
                    + "] is already scheduled - leaving the original job as-is.");
                return;
            }
        }

        JobDetail job = createJobDetail(name, groupName, jobClass, isVolatile, jobData);
        CronTrigger trigger = createCronTrigger(name, groupName, isVolatile, cronString, misfireInstruction);
        Date next = this.scheduler.scheduleJob(job, trigger);

        log.info("Scheduled cron job [" + name + ':' + groupName + "] to fire next at [" + next
            + "] with the cron string [" + cronString + "].");

        return;
    }

    /**
     * @see EnhancedScheduler#scheduleSimpleRepeatingJob(Class, boolean, boolean, long, long)
     */
    public void scheduleSimpleRepeatingJob(Class<? extends Job> jobClass, boolean rescheduleIfExists,
        boolean isVolatile, long initialDelay, long interval) throws SchedulerException {

        scheduleRepeatingJob(jobClass.getName(), jobClass.getName(), null, jobClass, rescheduleIfExists, isVolatile,
            initialDelay, interval);
        return;
    }

    /**
     * @see EnhancedScheduler#scheduleSimpleCronJob(Class, boolean, boolean, String)
     */
    public void scheduleSimpleCronJob(Class<? extends Job> jobClass, boolean rescheduleIfExists, boolean isVolatile,
        String cronString, Integer misfireInstruction) throws SchedulerException {

        scheduleCronJob(jobClass.getName(), jobClass.getName(), null, jobClass, rescheduleIfExists, isVolatile,
            cronString, misfireInstruction);
        return;
    }

    //---------------------------------------------------------------------
    //-- Scheduler interface methods
    //---------------------------------------------------------------------

    public String getSchedulerName() throws SchedulerException {
        return this.scheduler.getSchedulerName();
    }

    public String getSchedulerInstanceId() throws SchedulerException {
        return this.scheduler.getSchedulerInstanceId();
    }

    public SchedulerContext getContext() throws SchedulerException {
        return this.scheduler.getContext();
    }

    public SchedulerMetaData getMetaData() throws SchedulerException {
        return this.scheduler.getMetaData();
    }

    public void start() throws SchedulerException {
        this.scheduler.start();
    }

    public void pause() throws SchedulerException {
        this.scheduler.pause();
    }

    public boolean isPaused() throws SchedulerException {
        return this.scheduler.isPaused();
    }

    public void shutdown() throws SchedulerException {
        this.scheduler.shutdown();
    }

    public void shutdown(boolean waitForJobsToComplete) throws SchedulerException {
        this.scheduler.shutdown(waitForJobsToComplete);
    }

    public boolean isShutdown() throws SchedulerException {
        return this.scheduler.isShutdown();
    }

    public List getCurrentlyExecutingJobs() throws SchedulerException {
        return this.scheduler.getCurrentlyExecutingJobs();
    }

    public Date scheduleJob(JobDetail jobDetail, Trigger trigger) throws SchedulerException {
        if (log.isDebugEnabled()) {
            log.debug("Job details: " + jobDetail);
        }

        return this.scheduler.scheduleJob(jobDetail, trigger);
    }

    public Date scheduleJob(Trigger trigger) throws SchedulerException {
        return this.scheduler.scheduleJob(trigger);
    }

    public void addJob(JobDetail jobDetail, boolean replace) throws SchedulerException {
        this.scheduler.addJob(jobDetail, replace);
    }

    public boolean deleteJob(String jobName, String groupName) throws SchedulerException {
        return this.scheduler.deleteJob(jobName, groupName);
    }

    public boolean unscheduleJob(String triggerName, String groupName) throws SchedulerException {
        return this.scheduler.unscheduleJob(triggerName, groupName);
    }

    public void triggerJob(String jobName, String groupName) throws SchedulerException {
        this.scheduler.triggerJob(jobName, groupName);
    }

    public void triggerJobWithVolatileTrigger(String jobName, String groupName) throws SchedulerException {
        this.scheduler.triggerJobWithVolatileTrigger(jobName, groupName);
    }

    public void pauseTrigger(String triggerName, String groupName) throws SchedulerException {
        this.scheduler.pauseTrigger(triggerName, groupName);
    }

    public void pauseTriggerGroup(String groupName) throws SchedulerException {
        this.scheduler.pauseTriggerGroup(groupName);
    }

    public void pauseJob(String jobName, String groupName) throws SchedulerException {
        this.scheduler.pauseJob(jobName, groupName);
    }

    public void pauseJobGroup(String groupName) throws SchedulerException {
        this.scheduler.pauseJobGroup(groupName);
    }

    public void resumeTrigger(String triggerName, String groupName) throws SchedulerException {
        this.scheduler.resumeTrigger(triggerName, groupName);
    }

    public void resumeTriggerGroup(String groupName) throws SchedulerException {
        this.scheduler.resumeTriggerGroup(groupName);
    }

    public void resumeJob(String jobName, String groupName) throws SchedulerException {
        this.scheduler.resumeJob(jobName, groupName);
    }

    public void resumeJobGroup(String groupName) throws SchedulerException {
        this.scheduler.resumeJobGroup(groupName);
    }

    public String[] getJobGroupNames() throws SchedulerException {
        return this.scheduler.getJobGroupNames();
    }

    public String[] getJobNames(String groupName) throws SchedulerException {
        return this.scheduler.getJobNames(groupName);
    }

    public Trigger[] getTriggersOfJob(String jobName, String groupName) throws SchedulerException {
        return this.scheduler.getTriggersOfJob(jobName, groupName);
    }

    public String[] getTriggerGroupNames() throws SchedulerException {
        return this.scheduler.getTriggerGroupNames();
    }

    public String[] getTriggerNames(String groupName) throws SchedulerException {
        return this.scheduler.getTriggerNames(groupName);
    }

    public JobDetail getJobDetail(String jobName, String jobGroup) throws SchedulerException {
        return this.scheduler.getJobDetail(jobName, jobGroup);
    }

    public Trigger getTrigger(String triggerName, String triggerGroup) throws SchedulerException {
        return this.scheduler.getTrigger(triggerName, triggerGroup);
    }

    public boolean deleteCalendar(String calName) throws SchedulerException {
        return this.scheduler.deleteCalendar(calName);
    }

    public Calendar getCalendar(String calName) throws SchedulerException {
        return this.scheduler.getCalendar(calName);
    }

    public String[] getCalendarNames() throws SchedulerException {
        return this.scheduler.getCalendarNames();
    }

    public void addGlobalJobListener(JobListener jobListener) throws SchedulerException {
        this.scheduler.addGlobalJobListener(jobListener);
    }

    public void addJobListener(JobListener jobListener) throws SchedulerException {
        this.scheduler.addJobListener(jobListener);
    }

    public boolean removeGlobalJobListener(JobListener jobListener) throws SchedulerException {
        return this.scheduler.removeGlobalJobListener(jobListener);
    }

    public boolean removeJobListener(String name) throws SchedulerException {
        return this.scheduler.removeJobListener(name);
    }

    public List getGlobalJobListeners() throws SchedulerException {
        return this.scheduler.getGlobalJobListeners();
    }

    public Set getJobListenerNames() throws SchedulerException {
        return this.scheduler.getJobListenerNames();
    }

    public JobListener getJobListener(String name) throws SchedulerException {
        return this.scheduler.getJobListener(name);
    }

    public void addGlobalTriggerListener(TriggerListener triggerListener) throws SchedulerException {
        this.scheduler.addGlobalTriggerListener(triggerListener);
    }

    public void addTriggerListener(TriggerListener triggerListener) throws SchedulerException {
        this.scheduler.addTriggerListener(triggerListener);
    }

    public boolean removeGlobalTriggerListener(TriggerListener triggerListener) throws SchedulerException {
        return this.scheduler.removeGlobalTriggerListener(triggerListener);
    }

    public boolean removeTriggerListener(String name) throws SchedulerException {
        return this.scheduler.removeTriggerListener(name);
    }

    public List getGlobalTriggerListeners() throws SchedulerException {
        return this.scheduler.getGlobalTriggerListeners();
    }

    public Set getTriggerListenerNames() throws SchedulerException {
        return this.scheduler.getTriggerListenerNames();
    }

    public TriggerListener getTriggerListener(String name) throws SchedulerException {
        return this.scheduler.getTriggerListener(name);
    }

    public void addSchedulerListener(SchedulerListener schedulerListener) throws SchedulerException {
        this.scheduler.addSchedulerListener(schedulerListener);
    }

    public boolean removeSchedulerListener(SchedulerListener schedulerListener) throws SchedulerException {
        return this.scheduler.removeSchedulerListener(schedulerListener);
    }

    public List getSchedulerListeners() throws SchedulerException {
        return this.scheduler.getSchedulerListeners();
    }

    // Quartz methods that are new in 1.5.1 that were not in 1.0.7

    public void addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)
        throws SchedulerException {
        this.scheduler.addCalendar(calName, calendar, replace, updateTriggers);
    }

    public Set getPausedTriggerGroups() throws SchedulerException {
        return this.scheduler.getPausedTriggerGroups();
    }

    public int getTriggerState(String triggerName, String triggerGroup) throws SchedulerException {
        return this.scheduler.getTriggerState(triggerName, triggerGroup);
    }

    public boolean interrupt(String jobName, String groupName) throws UnableToInterruptJobException {
        try {
            return this.scheduler.interrupt(jobName, groupName);
        } catch (SchedulerException e) {
            throw new UnableToInterruptJobException(e);
        }
    }

    public boolean isInStandbyMode() throws SchedulerException {
        return this.scheduler.isInStandbyMode();
    }

    public void pauseAll() throws SchedulerException {
        this.scheduler.pauseAll();
    }

    public Date rescheduleJob(String triggerName, String groupName, Trigger newTrigger) throws SchedulerException {
        return this.scheduler.rescheduleJob(triggerName, groupName, newTrigger);
    }

    public void resumeAll() throws SchedulerException {
        this.scheduler.resumeAll();
    }

    public void setJobFactory(JobFactory factory) throws SchedulerException {
        this.scheduler.setJobFactory(factory);
    }

    public void standby() throws SchedulerException {
        this.scheduler.standby();
    }

    public void triggerJob(String jobName, String groupName, JobDataMap data) throws SchedulerException {
        this.scheduler.triggerJob(jobName, groupName, data);
    }

    public void triggerJobWithVolatileTrigger(String jobName, String groupName, JobDataMap data)
        throws SchedulerException {
        this.scheduler.triggerJob(jobName, groupName, data);
    }

    /* new methods in quartz 1.6.5 below this line */
    public JobListener getGlobalJobListener(String jobName) throws SchedulerException {
        return this.scheduler.getGlobalJobListener(jobName);
    }

    public TriggerListener getGlobalTriggerListener(String triggerName) throws SchedulerException {
        return this.scheduler.getGlobalTriggerListener(triggerName);
    }

    public boolean isStarted() throws SchedulerException {
        return this.scheduler.isStarted();
    }

    public boolean removeGlobalJobListener(String jobName) throws SchedulerException {
        return this.scheduler.removeGlobalJobListener(jobName);
    }

    public boolean removeGlobalTriggerListener(String triggerName) throws SchedulerException {
        return this.scheduler.removeGlobalTriggerListener(triggerName);
    }

    public void startDelayed(int delay) throws SchedulerException {
        this.scheduler.startDelayed(delay);
    }

    private JobDetail getExistingJob(String name, String groupName, boolean rescheduleIfExists) {
        try {
            return this.scheduler.getJobDetail(name, groupName);
        } catch (SchedulerException e) {
            Throwable cause = e.getCause();
            if (cause instanceof ClassNotFoundException) {
                // This probably means the job class was not found in the classpath. Try to delete the invalid job.
                try {
                    deleteJob(name, groupName, null);
                } catch (SchedulerException e1) {
                    log.error("Failed to delete job [" + name + ":" + groupName + "] with invalid job class (as per "
                        + cause + ")", e1);
                }
            } else {
                log.error("Error while attempting to lookup job [" + name + ":" + groupName + "].", e);
            }
            // Fall through and hope we get lucky and are still able to schedule the job.
            return null;
        }
    }

    private void deleteJob(String name, String groupName, JobDetail job) throws SchedulerException {
        if (!this.scheduler.deleteJob(name, groupName)) {
            String jobString = (job != null) ? job.toString() : (name + ":" + groupName);
            throw new SchedulerException("Failed to delete job [" + jobString + "].");
        }
    }

    private JobDetail createJobDetail(String name, String groupName, Class<? extends Job> jobClass, boolean isVolatile,
        JobDataMap jobData) {
        JobDetail job = new JobDetail(name, groupName, jobClass, isVolatile, false, false);
        job.setJobDataMap(jobData);
        return job;
    }

    private SimpleTrigger createSimpleTrigger(String name, String groupName, boolean isVolatile, long initialDelay,
        long interval) {
        Date start = new Date(System.currentTimeMillis() + initialDelay);
        SimpleTrigger trigger = new SimpleTrigger(name, groupName, start, null, SimpleTrigger.REPEAT_INDEFINITELY,
            interval);
        trigger.setVolatility(isVolatile);
        return trigger;
    }

    private CronTrigger createCronTrigger(String name, String groupName, boolean isVolatile, String cronString,
        Integer misfireInstruction) throws SchedulerException {
        CronTrigger trigger;
        try {
            trigger = new CronTrigger(name, groupName, name, groupName, cronString);
            if (null != misfireInstruction) {
                trigger.setMisfireInstruction(misfireInstruction.intValue());
            }
        } catch (ParseException e) {
            throw new SchedulerException(e);
        }
        trigger.setVolatility(isVolatile);
        return trigger;
    }

    @Override
    public void scheduleTriggeredJob(Class<? extends Job> jobClass, boolean isVolatile, Trigger trigger)
        throws SchedulerException {

        String name = jobClass.getName();
        String groupName = name;

        // See if the job exists, if not, add it.
        JobDetail existingJob = getExistingJob(name, groupName, false);
        if (null == existingJob) {
            JobDetail job = new JobDetail(name, groupName, jobClass, isVolatile, true, false);
            this.scheduler.addJob(job, false);
        }

        if (null != trigger) {
            if (null == existingJob) {
                existingJob = getExistingJob(name, groupName, false);
            }
            this.scheduler.scheduleJob(existingJob, trigger);
        }

        return;
    }
}