/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.ode.bpel.engine.cron;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;

import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.engine.Contexts;
import org.apache.ode.bpel.engine.BpelServerImpl.ContextsAware;
import org.apache.ode.bpel.iapi.ClusterAware;
import org.apache.ode.bpel.iapi.ProcessConf;
import org.apache.ode.bpel.iapi.ProcessConf.CronJob;
import org.apache.ode.bpel.iapi.Scheduler.JobDetails;
import org.apache.ode.bpel.iapi.Scheduler.MapSerializableRunnable;
import org.apache.ode.utils.CronExpression;

public class CronScheduler {
    static final Log __log = LogFactory.getLog(CronScheduler.class);

    // minimum interval of the cron job(1 second)
    private final long MIN_INTERVAL = 0;
    // if the work is schedule too late from the due time, skip it
    private final long TOLERABLE_SCHEDULE_DELAY = 0;

    private ExecutorService _scheduledTaskExec;
    
    private Contexts _contexts;
    
    private final Timer _schedulerTimer = new Timer("CronScheduler", true);

    private final Collection<TerminationListener> _systemTerminationListeners = new ArrayList<TerminationListener>();
    
    private final Map<QName, Collection<TerminationListener>> _terminationListenersByPid = new HashMap<QName, Collection<CronScheduler.TerminationListener>>();
    
    private volatile boolean _shuttingDown = false;
    
    public void setScheduledTaskExec(ExecutorService taskExec) {
        _scheduledTaskExec = taskExec;
    }
    
    public void setContexts(Contexts _contexts) {
        this._contexts = _contexts;
    }
    
    public void shutdown() {
        _shuttingDown = true;
        _schedulerTimer.cancel();

        for( TerminationListener listener : _systemTerminationListeners ) {
            listener.terminate();
        }
        _systemTerminationListeners.clear();

        for( Collection<TerminationListener> listeners : _terminationListenersByPid.values() ) {
            for( TerminationListener listener : listeners ) {
                listener.terminate();
            }
        }
        _terminationListenersByPid.clear();
    }
    
    public void cancelProcessCronJobs(QName pid, boolean undeployed) {
        assert pid != null;
        
        if( __log.isDebugEnabled() ) {
        	__log.debug("Cancelling PROCESS CRON jobs for: " + pid);
        }
        Collection<TerminationListener> listenersToTerminate = new ArrayList<TerminationListener>();

        synchronized( _terminationListenersByPid ) {
            Collection<TerminationListener> listeners = _terminationListenersByPid.get(pid);
            if( listeners != null ) {
                listenersToTerminate.addAll(listeners);
                listeners.clear();
            }
            if( undeployed ) {
                _terminationListenersByPid.remove(pid);
            }
        }
        
        // terminate existing cron jobs if there are
        synchronized( pid ) {
            for( TerminationListener listener : listenersToTerminate ) {
                listener.terminate();
            }
        }
        
        __log.info("Cancelled PROCESS CRON jobs for: " + pid);

    }
    
    public void scheduleProcessCronJobs(QName pid, ProcessConf pconf) {
        if( _shuttingDown ) {
            return;
        }
        assert pid != null;
        
        cancelProcessCronJobs(pid, false);
        Collection<TerminationListener> newListeners = new ArrayList<TerminationListener>();
        
        synchronized( pid ) {
            if( __log.isDebugEnabled() ) {
            	__log.debug("Scheduling PROCESS CRON jobs for: " + pid);
            }
            
            // start new cron jobs
            for( final CronJob job : pconf.getCronJobs() ) {
                if( __log.isDebugEnabled() ) __log.debug("Scheduling PROCESS CRON job: " + job.getCronExpression() + " for: " + pid);
                // for each different scheduled time
                Runnable runnable = new Runnable() {
                    public void run() {
                        if( __log.isDebugEnabled() ) __log.debug("Running cron cleanup with details list size: " + job.getRunnableDetailList().size());
                        for( JobDetails details : job.getRunnableDetailList() ) {
                            try {
                                // for each clean up for the scheduled time
                                RuntimeDataCleanupRunnable cleanup = new RuntimeDataCleanupRunnable();
                                cleanup.restoreFromDetails(details);
                                cleanup.setContexts(_contexts);
                                cleanup.run();
                                if( __log.isDebugEnabled() ) __log.debug("Finished running runtime data cleanup from a PROCESS CRON job: " + cleanup);
                            } catch(Exception re) {
                                __log.error("Error during runtime data cleanup from a PROCESS CRON: " + details + "; check your cron settings in deploy.xml.", re);
                                // don't sweat.. the rest of the system and other cron jobs still should work
                            }
                        }
                    }
                };
                newListeners.add(schedule(job.getCronExpression(), runnable, null, null));
            }
        }
        
        // make sure the pid does not get into the terminationListener map if no cron is setup 
        if( !newListeners.isEmpty() ) {
            synchronized( _terminationListenersByPid ) {
                Collection<TerminationListener> oldListeners = _terminationListenersByPid.get(pid);
                if( oldListeners == null ) {
                    _terminationListenersByPid.put(pid, newListeners);
                } else {
                    oldListeners.addAll(newListeners);
                }
            }
        }
        
       __log.info("Scheduled PROCESS CRON jobs for: " + pid);
    }
    
    public void refreshSystemCronJobs(SystemSchedulesConfig systemSchedulesConf) {
        if( _shuttingDown ) {
            return;
        }

        synchronized( _systemTerminationListeners) {
            if( __log.isDebugEnabled() ) __log.debug("Refreshing SYSTEM CRON jobs.");

            try {
                // if error thrown on reading the schedules.xml, do not cancel existing cron jobs
                List<CronJob> systemCronJobs = systemSchedulesConf.getSystemCronJobs();
                
                // cancel cron jobs
                for( TerminationListener listener : _systemTerminationListeners ) {
                    listener.terminate();
                }
                _systemTerminationListeners.clear();
                
                // start new cron jobs
                for( final CronJob job : systemCronJobs ) {
                    if( __log.isDebugEnabled() ) __log.debug("Scheduling SYSTEM CRON job:" + job);
                    // for each different scheduled time
                    Runnable runnable = new Runnable() {
                        public void run() {
                            for( JobDetails details : job.getRunnableDetailList() ) {
                                try {
                                    // for now, we have only runtime data cleanup cron job defined
                                    // for each clean up for the scheduled time
                                    RuntimeDataCleanupRunnable cleanup = new RuntimeDataCleanupRunnable();
                                    synchronized( _terminationListenersByPid ) {
                                        if( !_terminationListenersByPid.isEmpty() ) {
                                            details.getDetailsExt().put("pidsToExclude", _terminationListenersByPid.keySet());
                                        }
                                    }
                                    cleanup.restoreFromDetails(details);
                                    cleanup.setContexts(_contexts);
                                    cleanup.run();
                                    if( __log.isDebugEnabled() ) __log.debug("Finished running runtime data cleanup from a SYSTEM CRON job:" + cleanup);
                                } catch( Exception e ) {
                                    __log.error("Error running a runtime data cleanup from a SYSTEM CRON job: " + details + "; check your system cron setup.", e);
                                }
                            }
                        }
                    };
                    _systemTerminationListeners.add(schedule(job.getCronExpression(), runnable, null, null));
                }
            } catch( Exception e ) {
                __log.error("Error during refreshing SYSTEM CRON schedules: ", e);
            }
        }
    }

    public TerminationListener schedule(final CronExpression cronExpression, 
            final Runnable runnable, final JobDetails runnableDetails, 
            TerminationListener terminationListener) {
        if( _shuttingDown ) {
            __log.info("CRON Scheduler is being shut down. This new scheduling request is ignored.");
            return new TerminationListener() {
                public void terminate() {
                    // do nothing
                }
            };
        }
        
        assert cronExpression != null;
        assert runnable != null;
        
        final Date nextScheduleTime = cronExpression.getNextValidTimeAfter(new Date(
                System.currentTimeMillis() + MIN_INTERVAL));
        final CronScheduledJob job = new CronScheduledJob(nextScheduleTime, runnable, runnableDetails, cronExpression, terminationListener);
        if( __log.isDebugEnabled() ) __log.debug("CRON will run in " + (nextScheduleTime.getTime() - System.currentTimeMillis()) + "ms.");
        
        try {
            _schedulerTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    __log.debug("Cron scheduling timer kicked in: " + cronExpression);
                    // run only if the current node is the coordinator, 
                    // with the SimpleScheduler, the node is always the coordinator
                    if( !(_contexts.scheduler instanceof ClusterAware) 
                            || ((ClusterAware)_contexts.scheduler).amICoordinator() ) {
                        // do not hold the timer thread too long, submit the work to an executorService
                        _scheduledTaskExec.submit(job);
                        __log.debug("CRON job scheduled " + runnable);
                    }
                }
            }, nextScheduleTime);
        } catch( IllegalStateException ise ) {
            if( _shuttingDown ) {
                __log.info("CRON Scheduler is being shut down. This new scheduling request is ignored.");
            } else {
                throw ise;
            }
        }

        return job.terminationListener;
    }
    
    public interface TerminationListener {
        void terminate();
    }
    
    private class CronScheduledJob implements Callable<TerminationListener> {
        private volatile boolean terminated = false;
        private Date nextScheduleTime;
        private Runnable runnable;
        private JobDetails runnableDetails;
        private CronExpression cronExpression;
        private TerminationListener terminationListener;
        
        public CronScheduledJob(Date nextScheduleTime,
                Runnable runnable, JobDetails runnableDetails,
                CronExpression cronExpression, TerminationListener terminationListener) {
            this.nextScheduleTime = nextScheduleTime;
            this.runnable = runnable;
            this.runnableDetails = runnableDetails;
            this.cronExpression = cronExpression;
            if( terminationListener == null ) {
                terminationListener = new TerminationListener() {
                    public void terminate() {
                        terminated = true;
                    }
                };
            }
            this.terminationListener = terminationListener;
        }

        public TerminationListener call() throws Exception {
            try {
                if( TOLERABLE_SCHEDULE_DELAY == 0 ||
                    nextScheduleTime.getTime() < System.currentTimeMillis() + TOLERABLE_SCHEDULE_DELAY) {
                    if( runnableDetails != null && 
                            runnable instanceof MapSerializableRunnable ) {
                        ((MapSerializableRunnable)runnable).restoreFromDetails(runnableDetails);
                    }
                    if (runnable instanceof ContextsAware) {
                        ((ContextsAware) runnable).setContexts(_contexts);
                    }
                    if( !_shuttingDown && !terminated ) {
                        __log.debug("Running CRON job: " + runnable + " for " + nextScheduleTime.getTime());
                        runnable.run();
                    }
                } else {
                    // ignore the scheduling.. it will be scheduled later
                }
            } catch( Exception e ) {
                if( _shuttingDown ) {
                    __log.info("A cron job threw an Exception during ODE shutdown: " + e.getMessage() + ", you can ignore the error.");
                } else if( e instanceof RuntimeException ) {
                    throw e;
                } else {
                    throw new RuntimeException("Exception during running cron scheduled job: " + runnable, e);
                }
            } finally {
                if( !_shuttingDown && !terminated ) {
                    schedule(cronExpression, runnable, runnableDetails, terminationListener);
                }
            }
            
            return terminationListener;
        }
    }
}
