package net.i2p.util;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;

import net.i2p.I2PAppContext;

/**
 * Simple event scheduler - toss an event on the queue and it gets fired at the
 * appropriate time.  The method that is fired however should NOT block (otherwise
 * they b0rk the timer).
 *
 * This is like SimpleTimer but addEvent() for an existing event adds a second
 * job. Unlike SimpleTimer, events cannot be cancelled or rescheduled.
 *
 * For events that cannot or will not be cancelled or rescheduled -
 * for example, a call such as:
 *       SimpleTimer.getInstance().addEvent(new FooEvent(bar), timeoutMs);
 * use SimpleScheduler instead to reduce lock contention in SimpleTimer...
 *
 * For periodic events, use addPeriodicEvent(). Unlike SimpleTimer,
 * uncaught Exceptions will not prevent subsequent executions.
 *
 * @deprecated in 0.9.20, use SimpleTimer2 instead
 *
 * @author zzz
 */
@Deprecated
public class SimpleScheduler {

    /**
     *  If you have a context, use context.simpleScheduler() instead
     *  @deprecated in 0.9.20, replaced by SimpleTimer2
     */
    @Deprecated
    public static SimpleScheduler getInstance() {
        return I2PAppContext.getGlobalContext().simpleScheduler();
    }

    private static final int MIN_THREADS = 2;
    private static final int MAX_THREADS = 4;
    private final Log _log;
    private final ScheduledThreadPoolExecutor _executor;
    private final String _name;
    private int _count;
    private final int _threads;

    /**
     *  To be instantiated by the context.
     *  Others should use context.simpleTimer() instead
     *  @deprecated in 0.9.20, replaced by SimpleTimer2
     */
    @Deprecated
    public SimpleScheduler(I2PAppContext context) {
        this(context, "SimpleScheduler");
    }

    /**
     *  To be instantiated by the context.
     *  Others should use context.simpleTimer() instead
     *  @deprecated in 0.9.20, replaced by SimpleTimer2
     */
    @Deprecated
    private SimpleScheduler(I2PAppContext context, String name) {
        _log = context.logManager().getLog(SimpleScheduler.class);
        _name = name;
        long maxMemory = SystemVersion.getMaxMemory();
        _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
        _executor = new ScheduledThreadPoolExecutor(_threads, new CustomThreadFactory());
        _executor.prestartAllCoreThreads();
        // don't bother saving ref to remove hook if somebody else calls stop
        context.addShutdownTask(new Shutdown());
    }
    
    /**
     * @since 0.8.8
     */
    private class Shutdown implements Runnable {
        public void run() {
            stop();
        }
    }

    /**
     * Stops the SimpleScheduler.
     * Subsequent executions should not throw a RejectedExecutionException.
     */
    public void stop() {
        _executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        _executor.shutdownNow();
    }

    /**
     * Queue up the given event to be fired no sooner than timeoutMs from now.
     *
     * @param event
     * @param timeoutMs 
     */
    public void addEvent(SimpleTimer.TimedEvent event, long timeoutMs) {
        if (event == null)
            throw new IllegalArgumentException("addEvent null");
        RunnableEvent re = new RunnableEvent(event, timeoutMs);
        re.schedule();
    }
    
    /**
     * Queue up the given event to be fired after timeoutMs and every
     * timeoutMs thereafter. The TimedEvent must not do its own rescheduling.
     * As all Exceptions are caught in run(), these will not prevent
     * subsequent executions (unlike SimpleTimer, where the TimedEvent does
     * its own rescheduling).
     */
    public void addPeriodicEvent(SimpleTimer.TimedEvent event, long timeoutMs) {
        addPeriodicEvent(event, timeoutMs, timeoutMs);
    }
    
    /**
     * Queue up the given event to be fired after initialDelay and every
     * timeoutMs thereafter. The TimedEvent must not do its own rescheduling.
     * As all Exceptions are caught in run(), these will not prevent
     * subsequent executions (unlike SimpleTimer, where the TimedEvent does
     * its own rescheduling)
     *
     * @param event
     * @param initialDelay (ms)
     * @param timeoutMs 
     */
    public void addPeriodicEvent(SimpleTimer.TimedEvent event, long initialDelay, long timeoutMs) {
        if (event == null)
            throw new IllegalArgumentException("addEvent null");
        RunnableEvent re = new PeriodicRunnableEvent(event, initialDelay, timeoutMs);
        re.schedule();
    }
    
    private class CustomThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            Thread rv = Executors.defaultThreadFactory().newThread(r);
            rv.setName(_name +  ' ' + (++_count) + '/' + _threads);
// Uncomment this to test threadgrouping, but we should be all safe now that the constructor preallocates!
//            String name = rv.getThreadGroup().getName();
//            if(!name.equals("main")) {
//                (new Exception("OWCH! DAMN! Wrong ThreadGroup `" + name +"', `" + rv.getName() + "'")).printStackTrace();
//            }
            rv.setDaemon(true);
            return rv;
        }
    }

    /**
     * Same as SimpleTimer.TimedEvent but use run() instead of timeReached(), and remembers the time
     */
    private class RunnableEvent implements Runnable {
        protected final SimpleTimer.TimedEvent _timedEvent;
        protected long _scheduled;

        public RunnableEvent(SimpleTimer.TimedEvent t, long timeoutMs) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Creating w/ delay " + timeoutMs + " : " + t);
            _timedEvent = t;
            _scheduled = timeoutMs + System.currentTimeMillis();
        }
        public void schedule() {
            _executor.schedule(this, _scheduled - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }
        public void run() {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Running: " + _timedEvent);
            long before = System.currentTimeMillis();
            if (_log.shouldLog(Log.WARN) && before < _scheduled - 100)
                _log.warn(_name + " early execution " + (_scheduled - before) + ": " + _timedEvent);
            else if (_log.shouldLog(Log.WARN) && before > _scheduled + 1000)
                _log.warn("late execution " + (before - _scheduled) + ": " + _timedEvent + debug());
            try {
                _timedEvent.timeReached();
            } catch (Throwable t) {
                _log.log(Log.CRIT, _name + ": Scheduled task " + _timedEvent + " exited unexpectedly, please report", t);
            }
            long time = System.currentTimeMillis() - before;
            if (time > 1000 && _log.shouldLog(Log.WARN))
                _log.warn(_name + " event execution took " + time + ": " + _timedEvent);
            if (_log.shouldLog(Log.INFO)) {
                 // this call is slow - iterates through a HashMap -
                 // would be better to have a local AtomicLong if we care
                 long completed = _executor.getCompletedTaskCount();
                 if (completed % 250 == 0)
                     _log.info(debug());
            }
        }
    }

    /** Run every timeoutMs. TimedEvent must not do its own reschedule via addEvent() */
    private class PeriodicRunnableEvent extends RunnableEvent {
        private final long _timeoutMs;
        private final long _initialDelay;
        public PeriodicRunnableEvent(SimpleTimer.TimedEvent t, long initialDelay, long timeoutMs) {
            super(t, timeoutMs);
            _initialDelay = initialDelay;
            _timeoutMs = timeoutMs;
            _scheduled = initialDelay + System.currentTimeMillis();
        }
        @Override
        public void schedule() {
            _executor.scheduleWithFixedDelay(this, _initialDelay, _timeoutMs, TimeUnit.MILLISECONDS);
        }
        @Override
        public void run() {
            super.run();
            _scheduled = _timeoutMs + System.currentTimeMillis();
        }
    }

    private String debug() {
        return
            " Pool: " + _name +
            " Active: " + _executor.getActiveCount() + '/' + _executor.getPoolSize() +
            " Completed: " + _executor.getCompletedTaskCount() +
            " Queued: " + _executor.getQueue().size();
    }
}

