package net.i2p.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

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).
 *
 * WARNING - Deprecated.
 * This is an inefficient mess. Use  SimpleTimer2 if possible.
 */
public class SimpleTimer {

    /**
     *  If you have a context, use context.simpleTimer() instead
     *  @deprecated use SimpleTimer2
     */
	@Deprecated
    public static SimpleTimer getInstance() {
        return I2PAppContext.getGlobalContext().simpleTimer();
    }

    private final Log _log;
    /** event time (Long) to event (TimedEvent) mapping */
    private final TreeMap<Long, TimedEvent> _events;
    /** event (TimedEvent) to event time (Long) mapping */
    private final Map<TimedEvent, Long> _eventTimes;
    private final List<TimedEvent> _readyEvents;
    private SimpleStore runn;
    private static final int MIN_THREADS = 2;
    private static final int MAX_THREADS = 4;

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

    /**
     *  To be instantiated by the context.
     *  Others should use context.simpleTimer() instead
     *  @deprecated use SimpleTimer2
     */
    @Deprecated
    private SimpleTimer(I2PAppContext context, String name) {
        runn = new SimpleStore(true);
        _log = context.logManager().getLog(SimpleTimer.class);
        _events = new TreeMap<Long, TimedEvent>();
        _eventTimes = new HashMap<TimedEvent, Long>(256);
        _readyEvents = new ArrayList<TimedEvent>(4);
        I2PThread runner = new I2PThread(new SimpleTimerRunner());
        runner.setName(name);
        runner.setDaemon(true);
        runner.start();
        long maxMemory = SystemVersion.getMaxMemory();
        int threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
        for (int i = 1; i <= threads ; i++) {
            I2PThread executor = new I2PThread(new Executor(context, _log, _readyEvents, runn));
            executor.setName(name + "Executor " + i + '/' + threads);
            executor.setDaemon(true);
            executor.start();
        }
        context.addShutdownTask(new Shutdown());
    }
    
    /**
     * @since 0.8.8
     */
    private class Shutdown implements Runnable {
        public void run() {
            removeSimpleTimer();
        }
    }

    /**
     * Removes the SimpleTimer.
     */
    public void removeSimpleTimer() {
        synchronized(_events) {
            runn.setAnswer(false);
            _events.clear();
            _eventTimes.clear();
            _events.notifyAll();
        }
        synchronized (_readyEvents) {
            _readyEvents.clear();
            _readyEvents.notifyAll();
        }
    }

    /**
     * 
     * @param event
     * @param timeoutMs
     */
    public void reschedule(TimedEvent event, long timeoutMs) {
        addEvent(event, timeoutMs, false);
    }
    
    /**
     * Queue up the given event to be fired no sooner than timeoutMs from now.
     * However, if this event is already scheduled, the event will be scheduled
     * for the earlier of the two timeouts, which may be before this stated 
     * timeout.  If this is not the desired behavior, call removeEvent first.
     *
     * @param event
     * @param timeoutMs 
     */
    public void addEvent(TimedEvent event, long timeoutMs) { addEvent(event, timeoutMs, true); }
    /**
     * @param event 
     * @param timeoutMs 
     * @param useEarliestTime if its already scheduled, use the earlier of the 
     *                        two timeouts, else use the later
     */
    public void addEvent(TimedEvent event, long timeoutMs, boolean useEarliestTime) {
        int totalEvents = 0;
        long now = System.currentTimeMillis();
        long eventTime = now + timeoutMs;
        Long time = Long.valueOf(eventTime);
        synchronized (_events) {
            // remove the old scheduled position, then reinsert it
            Long oldTime = _eventTimes.get(event);
            if (oldTime != null) {
                if (useEarliestTime) {
                    if (oldTime.longValue() < eventTime) {
                        _events.notifyAll();
                        return; // already scheduled for sooner than requested
                    } else {
                        _events.remove(oldTime);
                    }
                } else {
                    if (oldTime.longValue() > eventTime) {
                        _events.notifyAll();
                        return; // already scheduled for later than the given period
                    } else {
                        _events.remove(oldTime);
                    }
                }
            }
            // FIXME if you plan to use this class again
            while (_events.containsKey(time))
                time = Long.valueOf(time.longValue() + 1);
            _events.put(time, event);
            _eventTimes.put(event, time);
            
            if ( (_events.size() != _eventTimes.size()) ) {
                _log.error("Skewed events: " + _events.size() + " for " + _eventTimes.size());
                for (TimedEvent evt : _eventTimes.keySet()) {
                    Long when = _eventTimes.get(evt);
                    TimedEvent cur = _events.get(when);
                    if (cur != evt) {
                        _log.error("event " + evt + " @ " + when + ": " + cur);
                    }
                }
            }
            
            totalEvents = _events.size();
            _events.notifyAll();
        }
        if (time.longValue() > eventTime + 100) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Lots of timer congestion, had to push " + event + " back "
                           + (time.longValue()-eventTime) + "ms (# events: " + totalEvents + ")");
        }
        long timeToAdd = System.currentTimeMillis() - now;
        if (timeToAdd > 50) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("timer contention: took " + timeToAdd + "ms to add a job with " + totalEvents + " queued");
        }
            
    }
    
    public boolean removeEvent(TimedEvent evt) {
        if (evt == null) return false;
        synchronized (_events) {
            Long when = _eventTimes.remove(evt);
            if (when != null)
                _events.remove(when);
            return null != when;
        }
    }
    
    /**
     * Simple interface for events to be queued up and notified on expiration
     */
    public interface TimedEvent {
        /** 
         * the time requested has been reached (this call should NOT block,
         * otherwise the whole SimpleTimer gets backed up)
         *
         */
        public void timeReached();
    }
    
    private long _occurredTime;
    private long _occurredEventCount;
    // not used
    //  private TimedEvent _recentEvents[] = new TimedEvent[5];
    private class SimpleTimerRunner implements Runnable {
        public void run() {
            List<TimedEvent> eventsToFire = new ArrayList<TimedEvent>(1);
            while(runn.getAnswer()) {
                try {
                    synchronized (_events) {
                        //if (_events.size() <= 0)
                        //    _events.wait();
                        //if (_events.size() > 100)
                        //    _log.warn("> 100 events!  " + _events.values());
                        long now = System.currentTimeMillis();
                        long nextEventDelay = -1;
                        Object nextEvent = null;
                        while(runn.getAnswer()) {
                            if(_events.isEmpty()) {
                                break;
                            }
                            Long when = _events.firstKey();
                            if (when.longValue() <= now) {
                                TimedEvent evt = _events.remove(when);
                                if (evt != null) {                            
                                    _eventTimes.remove(evt);
                                    eventsToFire.add(evt);
                                } 
                            } else {
                                nextEventDelay = when.longValue() - now;
                                nextEvent = _events.get(when);
                                break;
                            }
                        }
                        if (eventsToFire.isEmpty()) { 
                            if (nextEventDelay != -1) {
                                if (_log.shouldLog(Log.DEBUG))
                                    _log.debug("Next event in " + nextEventDelay + ": " + nextEvent);
                                _events.wait(nextEventDelay);
                            } else {
                                _events.wait();
                            }
                        }
                    }
                } catch (ThreadDeath td) {
                    return; // die
                } catch (InterruptedException ie) {
                    // ignore
                } catch (Throwable t) {
                    if (_log != null) {
                        _log.log(Log.CRIT, "Uncaught exception in the SimpleTimer!", t);
                    } else {
                        System.err.println("Uncaught exception in SimpleTimer");
                        t.printStackTrace();
                    }
                }
                
                long now = System.currentTimeMillis();
                now = now - (now % 1000);

                synchronized (_readyEvents) {
                    for (int i = 0; i < eventsToFire.size(); i++) 
                        _readyEvents.add(eventsToFire.get(i));
                    _readyEvents.notifyAll();
                }

                if (_occurredTime == now) {
                    _occurredEventCount += eventsToFire.size();
                } else {
                    _occurredTime = now;
                    if (_occurredEventCount > 2500) {
                        StringBuilder buf = new StringBuilder(128);
                        buf.append("Too many simpleTimerJobs (").append(_occurredEventCount);
                        buf.append(") in a second!");
                        _log.log(Log.WARN, buf.toString());
                    }
                    _occurredEventCount = 0;
                }

                eventsToFire.clear();
            }
        }
    }
}

