/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */


package org.ow2.jonas.lib.timer;

import java.util.ArrayList;
import java.util.Iterator;

import org.objectweb.util.monolog.api.BasicLevel;

/**
 * Clock thread for a TimerManager
 * Every second, decrement timers and launch action if expired
 */
class Clock extends Thread {

    private TimerManager tmgr;

    public Clock(TimerManager tmgr) {
        super("JonasClock");
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Clock constructor");
        }
        this.tmgr = tmgr;
    }

    public void run() {
        tmgr.clock();
    }
}

/**
 * Batch thread for a TimerManager
 * pocess all expired timers
 */
class Batch extends Thread {

    private TimerManager tmgr;

    public Batch(TimerManager tmgr) {
        super("JonasBatch");
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Batch constructor");
        }
        this.tmgr = tmgr;
    }

    public void run() {
        tmgr.batch();
    }
}

/**
 * A timer manager manages 2 lists of timers with 2 threads
 * One thread is a clock which decrements timers every second
 * and passes them when expired in a list of expired timers.
 * The other thread looks in the list of expired timers to process
 * them.
 */
public class TimerManager {

    // threads managing the service.
    private static Batch batchThread;
    private static Clock clockThread;

    private final static long PERIOD_MAX = 30000; // 30 sec
    private final static long PERIOD_MIN = 100; // 1/10 sec
    private long period;
    private long minremtime = PERIOD_MAX;

    // lists
    private ArrayList timerList = new ArrayList();
    private ArrayList expiredList = new ArrayList();

    private static TimerManager unique = null;
    private static boolean shuttingdown = false;

    /**
     * Constructor
     */
    private TimerManager() {
        // launch threads for timers
        batchThread = new Batch(this);
        batchThread.start();
        clockThread = new Clock(this);
        clockThread.start();
    }

    /**
     * Get an instance of the TimerManager
     */
    public static TimerManager getInstance() {
        if (unique == null) {
            unique = new TimerManager();
        }
        return unique;
    }

    /**
     * stop the service
     * @param force tell the manager NOT to wait for the timers to be completed
     */
    public static void stop(boolean force) {
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG,"Stop TimerManager");
        }
        shuttingdown = true;
        while (clockThread.isAlive() || batchThread.isAlive()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG,"TimerManager has stopped");
        }
    }

    public static void stop() {
        stop(true);
    }


    /**
     * update all timers in the list
     * each timer expired is put in a special list of expired timers
     * they will be processed then by the Batch Thread.
     */
    public void clock() {
        synchronized(timerList) {
            while (true) {
                // compute time to sleep
                if (shuttingdown) {
                    period = 1;
                } else {
                    period = PERIOD_MAX;
                    if (minremtime < period) {
                        period = minremtime < PERIOD_MIN ? PERIOD_MIN : minremtime;
                    }
                }
                // must sleep a little less than period
                try {
                    timerList.wait(period);
                } catch (InterruptedException e) {
                    TraceTimer.logger.log(BasicLevel.ERROR, "Timer interrupted");
                }
                int found = 0;
                boolean empty = true;
                minremtime = PERIOD_MAX;
                for (Iterator i = timerList.iterator(); i.hasNext(); ) {
                    TimerEvent t = (TimerEvent) i.next();
                    if (!t.isStopped()) {
                        empty = false;
                    }
                    long remtime = t.update();
                    if (remtime <= 0) {
                        if (t.valid()) {
                            expiredList.add(t);
                            found++;
                            if (t.ispermanent() && !shuttingdown) {
                                remtime = t.restart();
                                if (remtime < minremtime) {
                                    minremtime = remtime;
                                }
                            } else {
                                i.remove();
                            }
                        } else {
                            i.remove();
                        }
                    } else {
                        if (remtime < minremtime) {
                            minremtime = remtime;
                        }
                    }
                    // Be sure there is no more ref on bean in this local variable.
                    t = null;
                }
                if (found > 0) {
                    timerList.notifyAll();
                } else {
                    if (empty) {
                        if (shuttingdown) {
                            break;
                        }
                    }
                }
            }
            timerList.notifyAll();
        }
    }

    /**
     * process all expired timers
     */
    public void batch() {

        while (!(shuttingdown && timerList.isEmpty() && expiredList.isEmpty())) {
            TimerEvent t;
            synchronized(timerList) {
                while (expiredList.isEmpty()) {
                    if (shuttingdown) {
                        TraceTimer.logger.log(BasicLevel.WARN, "TimerManager shutting down");
                        return;
                    }
                    try {
                        timerList.wait();
                    } catch (Exception e) {
                        TraceTimer.logger.log(BasicLevel.ERROR, "Exception in Batch: ",e);
                    }
                }
                t = (TimerEvent) expiredList.remove(0);
            }
            // Do not keep the lock during the processing of the timer
            try {
                t.process();
            } catch (Exception e) {
                // An exception in a timer process should not stop this thread.
                TraceTimer.logger.log(BasicLevel.WARN, "Ignoring exception: " + e);
                e.printStackTrace();
            }
        }
        TraceTimer.logger.log(BasicLevel.WARN, "TimerManager stopped");
    }

    /**
     * add a new timer in the list
     * @param tel Object that will be notified when the timer expire.
     * @param timeout nb of seconds before the timer expires.
     * @param arg info passed with the timer
     * @param permanent true if the timer is permanent.
     * @deprecated use addTimerMs instead.
     */
    public TimerEvent addTimer(TimerEventListener tel, long timeout, Object arg, boolean permanent) {
        return addTimerMs(tel, timeout * 1000, arg, permanent);
    }

    /**
     * add a new timer in the list
     * @param tel Object that will be notified when the timer expire.
     * @param timeout nb of milliseconds before the timer expires.
     * @param arg info passed with the timer
     * @param permanent true if the timer is permanent.
     */
    public TimerEvent addTimerMs(TimerEventListener tel, long timeout, Object arg, boolean permanent) {
        TimerEvent te = new TimerEvent(tel, timeout, arg, permanent);
        synchronized(timerList) {
            timerList.add(te);
            if (timeout < minremtime) {
                minremtime = timeout;
            }
            timerList.notifyAll();
        }
        return te;
    }

    /**
     * remove a timer from the list. this is not very efficient.
     * A better way to do this is TimerEvent.unset()
     * @deprecated
     */
    public void removeTimer(TimerEvent te) {
        synchronized(timerList) {
            timerList.remove(timerList.indexOf(te));
        }
    }
}
