/*
 * 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 com.sun.jini.thread;

import com.sun.jini.config.Config;

import java.text.DateFormat;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;

/**
 * A Queue of timed tasks.  Each task implements {@link Runnable}.
 * Events can either be executed in the queue's thread or in their own thread.
 * <p>
 * A task is an object that implements <code>Runnable</code>.  It is
 * scheduled by invoking {@link #schedule(long, Runnable, WakeupManager.ThreadDesc)
 * schedule} with a time at which it should be run.  When that time
 * arrives (approximately) the task will be pulled off the queue and
 * have its {@link Runnable#run run} method invoked.  <p>
 *
 * A <code>schedule</code> request can specify a
 * {@link WakeupManager.ThreadDesc}, which will define the parameters
 * of a thread to be created to run the <code>Runnable</code>.  You can
 * specify the group, whether the thread is a daemon thread, and the priority.
 * Additionally you can use a subclass of <code>WakeupManager.ThreadDesc</code>
 * and override the {@link WakeupManager.ThreadDesc#thread thread} method
 * to further customize thread creation.
 * <p>
 * 
 * When a task is scheduled, a {@link WakeupManager.Ticket} is returned
 * that can be used to cancel the event if desired.
 * <p>
 * 
 * The queue requires its own thread, whose parameters can be defined
 * via a <code>ThreadDesc</code> if desired. The queue's thread
 * will be started when the first task is scheduled. If the queue
 * becomes empty the thread will be terminated after a 
 * <a href=#queueThreadTimeout>configurable delay</a>. The thread
 * will be re-started if a new task is scheduled.
 * <p>
 *
 * While it is theoretically possible to obtain the queue's thread and
 * interrupt it, the results of doing so are undefined. If a client
 * wishes to stop the queue's thread the client should either remove
 * all the tasks or call {@link #stop}. Note, calling
 * <code>stop</code> will cause future <code>schedule</code> calls to
 * fail with an <code>IllegalStateException</code>. <p>
 *
 * <a name="ConfigEntries">
 * <code>WakeupManager</code> supports the <code>queueThreadTimeout</code>
 * configuration entry, with the component
 * <code>com.sun.jini.thread.WakeupManager</code>.
 *
 * <a name="queueThreadTimeout">
 * <table summary="Describes the queueThreadTimeout configuration entry" 
 *                border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>queueThreadTimeout</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>long</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> 30,000 milliseconds
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> How long, in milliseconds, the queue's thread will be
 *       left running if there are no scheduled tasks. Must be
 *       a non-negative long value. This configuration entry is 
 *       consulted when the <code>WakeupManager</code> is initially created.
 *           
 * </table>
 * <p>
 *
 * This class uses the {@link Logger} named
 * <code>com.sun.jini.thread.WakeupManager</code> to log information at
 * the following logging levels: <p>
 * 
 * <table border=1 cellpadding=5
 *       summary="Describes logging performed by WakeupManager at different
 *	          logging levels">
 *
 * <tr> <th> Level <th> Description
 *
 * <tr> <td> SEVERE <td> exceptions thrown when we attempt to
 *                       create the queue's thread
 * 
 * <tr> <td> WARNING <td> exceptions thrown by the run methods of tasks, 
 *                     by the <code>ThreadDesc</code>'s of tasks, or
 *                     if the queue's thread is interrupted
 *
 * <tr> <td> FINEST <td> how many milliseconds until the next event
 *                       and when the queue's thread is stopped or started
 *
 * </table>
 *
 * 
 *
 * @see java.lang.Runnable */
public class WakeupManager {
    /** Component we pull configuration entries from and our logger name */
    private final static String COMPONENT_NAME = 
	"com.sun.jini.thread.WakeupManager";

    /** Default value for <code>queueThreadTimeout</code> */
    private final static long DEFAULT_QUEUE_THREAD_TIMEOUT = 30000;
    
    /**
     * If there are no registered tasks number of
     * milliseconds to wait before killing the kicker thread
     */
    private final long  queueThreadTimeout;

    /**
     * The queue. Also the object we use for locking, multi-threaded
     * access to all the other fields is arbitrated by synchronizing
     * on this object.
     */
    private final SortedSet contents = new java.util.TreeSet();

    /** <code>ThreadDesc</code> we use to create kicker threads */
    private final ThreadDesc	kickerDesc;

    /** The Runnable for the queue's thread */
    private final Kicker kicker = new Kicker();

    /** Next tie breaker ticket */
    private long        nextBreaker = 0;
    
    /** First item in contents */
    private Ticket      head = null;

    /** The queue's thread */
    private Thread	kickerThread;

    /** 
     * <code>true</code> if we have been stopped.
     */
    private boolean     dead = false;

    /**
     * <code>DataFormat</code> used by {@link Ticket} to format its
     * <code>toString</code> return value.
     */
    private static DateFormat dateFmt  = 
	DateFormat.getTimeInstance(DateFormat.LONG);

    /** Logger for this class and nested classes */
    private static final Logger logger = Logger.getLogger(COMPONENT_NAME);

    /**
     * Description of a future thread.
     *
     * @see WakeupManager#schedule
     * @see WakeupManager#WakeupManager(WakeupManager.ThreadDesc)
     */
    public static class ThreadDesc {
	private final ThreadGroup group;	// group to create in
	private final boolean daemon;		// create as daemon?
	private final int priority;		// priority

	/**
	 * Equivalent to
	 * <pre>
	 *     ThreadDesc(null, false)
	 * </pre>
	 */
	public ThreadDesc() {
	    this(null, false);
	}

	/**
	 * Equivalent to
	 * <pre>
	 *     ThreadDesc(group, deamon, Thread.NORM_PRIORITY)
	 * </pre>
	 */
	public ThreadDesc(ThreadGroup group, boolean daemon) {
	    this(group, daemon, Thread.NORM_PRIORITY);
	}

	/**
	 * Describe a future thread that will be created in the given group,
	 * deamon status, and priority.
	 *
	 * @param group The group to be created in.  If <code>null</code>,
	 *		the thread will be created in the default group.
	 * @param daemon The thread will be a daemon thread if this is
	 *		<code>true</code>.
	 * @param priority The thread's priority.
	 * @throws IllegalArgumentException if priority is not 
	 *    in between {@link Thread#MIN_PRIORITY} and 
	 *    {@link Thread#MAX_PRIORITY}
	 */
	public ThreadDesc(ThreadGroup group, boolean daemon, int priority) {
	    if (priority < Thread.MIN_PRIORITY ||
		priority > Thread.MAX_PRIORITY)
	    {
		throw new IllegalArgumentException("bad value for priority:" +
						   priority);
	    }

	    this.group = group;
	    this.daemon = daemon;
	    this.priority = priority;
	}

	/** 
	 * The {@link ThreadGroup} the thread will be created in.
	 * @return the {@link ThreadGroup} the thread will be created in.
	 */
	public ThreadGroup getGroup() { return group; }

	/** 
	 * Returns <code>true</code> if the the thread will be daemon
	 * thread, returns <code>false</code> otherwise.
	 * @return <code>true</code> if the the thread will be daemon
	 * thread, returns <code>false</code> otherwise.
	 */
	public boolean isDaemon() { return daemon; }

	/**
	 * The priority the thread should be created with.
	 * @return the priority the thread should be created with.
	 */
	public int getPriority() { return priority; }

	/**
	 * Create a thread for the given runnable based on the values in this
	 * object. May be overridden to give full control over creation
	 * of thread.
	 * @return a thread to run <code>r</code>, unstarted
	 */
	public Thread thread(Runnable r) {
	    Thread thr;
	    if (getGroup() == null)
		thr = new Thread(r);
	    else
		thr = new Thread(getGroup(), r);
	    thr.setDaemon(isDaemon());
	    thr.setPriority(getPriority());
	    return thr;
	}

	public String toString() {
	    return "[" + getGroup() + ", " + isDaemon() + ", " 
		+ getPriority() + "]";
	}
    }

    /**
     * A ticket that can be used for cancelling a future task.  It
     * describes the task itself as well. The {@link
     * WakeupManager#newTicket WakeupManager.newTicket} method
     * can be used by subclasses of <code>WakeupManager</code> to
     * create new <code>Ticket</code> instances. 
     */
    public static class Ticket implements Comparable {
	/** When the task should occur. */
	public final long when;
	/** The task object to be executed */
	public final Runnable task;
	/** The <code>ThreadDesc</code>, or <code>null</code> if none. */
	public final ThreadDesc desc;

	/** Tie beaker used when two tickets have the same value for when */
	private final long breaker;

        private Ticket(long when, Runnable task, ThreadDesc threadDesc,
		       long breaker)
	{
	    if (task == null)
		throw new NullPointerException("task not specified");
	    this.when = when;
	    this.task = task;
	    this.desc = threadDesc;
	    this.breaker = breaker;
	}

	public String toString() {
	    return dateFmt.format(Long.valueOf(when)) + "(" + when + ")" + ", "
		+ task.getClass().getName() + ", " + desc;
	}

	public boolean equals(Object o) {
	    if (!(o instanceof Ticket))
		return false;

	    final Ticket that = (Ticket)o;

	    return that.when == when && that.breaker == breaker;
	}

	public int hashCode() {
	    return (int)breaker;
	}

	public int compareTo(Object o) {
	    final Ticket that = (Ticket)o;	    
	    
	    final long whenDiff = when - that.when;
	    if (whenDiff > 0)
		return 1;
	    else if (whenDiff < 0)
		return -1;
	    else {
		final long breakerDiff = breaker - that.breaker;	

		if (breakerDiff > 0)
		    return 1;
		else if (breakerDiff < 0)
		    return -1;
		else
		    return 0;
	    }
	}
    }

    /**
     * Create a new <code>WakeupManager</code>. Equivalent to.
     * <pre>
     *     WakeupManager(new ThreadDesc())
     * </pre>
     *
     * @see WakeupManager.ThreadDesc
     */
    public WakeupManager() {
	this(new ThreadDesc());
    }

    /**
     * Create a new <code>WakeupManager</code>.  The thread used for
     * timing will be created according to the provided <code>ThreadDesc</code>.
     * @throws NullPointerException if desc is null
     */
    public WakeupManager(ThreadDesc desc) {
	if (desc == null)
	    throw new NullPointerException("desc must be non-null");

	kickerDesc = desc;
	queueThreadTimeout = DEFAULT_QUEUE_THREAD_TIMEOUT;
    }

    /**
     * Create a new <code>WakeupManager</code>.  The thread used for
     * timing will be created according to the provided <code>ThreadDesc</code>.
     * Optionally pass a configuration to control various implementation
     * specific behaviors.
     * @throws ConfigurationException if if an exception
     *         occurs while retrieving an item from the given
     *         <code>Configuration</code> object
     * @throws NullPointerException if either argument is null
     */
    public WakeupManager(ThreadDesc desc, Configuration config)
        throws ConfigurationException
    {
	if (desc == null)
	    throw new NullPointerException("desc must be non-null");

	kickerDesc = desc;
	queueThreadTimeout = Config.getLongEntry(config, COMPONENT_NAME, 
		"queueThreadTimeout", DEFAULT_QUEUE_THREAD_TIMEOUT, 
		0, Long.MAX_VALUE);
    }

    /**
     * Create a new ticket with the specified values for when the task
     * should be run, what task should be run, and what sort of
     * thread the task should be run in. 
     *
     * @param when when the task should run, an absolute time
     * @param task what task should be run
     * @param threadDesc if non-<code>null</code> the object to use to 
     *        create the thread the task should be run in, if
     *        <code>null</code> the task should be run in the
     *        manager's thread.
     * @throws NullPointerException if task is <code>null</code>
     */
    protected Ticket newTicket(long when, Runnable task, ThreadDesc threadDesc) {
	synchronized (contents) {
	    return new Ticket(when, task, threadDesc, nextBreaker++);
	}
    }

    /**
     * Schedule the given task for the given time.  The task's <code>run</code>
     * method will be executed synchronously in the queue's own thread, so it
     * should be brief or it will affect whether future events will be executed
     * at an appropriate time.
     * @throws NullPointerException if <code>task</code> is <code>null</code>
     * @throws IllegalStateException if the manager has been stopped
     */
    public Ticket schedule(long when, Runnable task) {
	return schedule(when, task, null);
    }

    /**
     * Schedule the given task for the given time, to be run in a thread.
     * When the time comes, a new thread will be created according to the
     * <code>ThreadDesc</code> object provided.  If <code>threadDesc</code> is
     * <code>null</code>, this is equivalent to the other form of
     * <code>schedule</code>.
     * @throws NullPointerException if <code>task</code> is <code>null</code>
     * @throws IllegalStateException if the manager has been stopped
     */
    public Ticket schedule(long when, Runnable task, ThreadDesc threadDesc) {
	synchronized (contents) {
	    if (dead)
		throw new IllegalStateException(
		    "trying to add task to stopped WakeupManager");

	    Ticket t = newTicket(when, task, threadDesc);
	    contents.add(t);
	    
	    if (kickerThread == null) {
		logger.log(Level.FINEST, "starting queue's thread");

		try {
		    final Thread thread = kickerDesc.thread(kicker);
		    thread.start();

		    // Only set once we know start worked
		    kickerThread = thread;
		} catch (Throwable tt) {
		    try {
			logger.log(Level.SEVERE, 
				   "queue thread creation exception",tt);
		    } catch (Throwable ttt) {
			// don't let a problem in logging kill the thread
		    }
		}
	    }

	    // need to call checkHead (even if we just (re)created the
	    // kickerThread), because that is how head gets set (note,
	    // this is ok to call even if thread creation failed)
	    checkHead();

	    return t;
	}
    }

    /**
     * Cancel the given ticket.
     */
    public void cancel(Ticket t) {
	synchronized (contents) {
	    if (dead) return;

	    contents.remove(t);
	    checkHead();
	}
    }

    /**
     * Cancel all tickets.
     */
    public void cancelAll() {
	synchronized (contents) {
	    if (dead) return;

	    contents.clear();
	    checkHead();
	}
    }


    /**
     * Called whenever we change contents to update head
     * and see if we need to wake up the queue thread.
     * Assumes the caller holds the lock on contents.
     */
    private void checkHead() {
	assert Thread.holdsLock(contents);
	final Ticket oldHead = head;

	if (contents.isEmpty())
	    head = null;
	else
	    head = (Ticket)contents.first();

	if (head == oldHead) return;

	// New first event (including possibly no events), run
	// needs to wake up and change its sleep time.
	contents.notifyAll();
    }

    /**
     * Return whether the queue is currently empty.
     */
    public boolean isEmpty() {
	synchronized (contents) {
	    return (contents.isEmpty());
	}
    }

    /**
     * Stop executing.
     */
    public void stop() {
	synchronized (contents) {
	    contents.clear();
	    kickerThread = null;
	    head = null;
	    dead = true;
	    contents.notifyAll();
	}
    }

    /**
     * The kicker work.  This is what sleeps until the time of
     * the next event.
     */
    private class Kicker implements Runnable {
	public void run() {
	    /* Set when contents is empty to control when the kicker will
	     * exit. Long.MIN_VALUE used as flag value to indicate 
	     * kickerExitTime is invalid
	     */
	    long kickerExitTime = Long.MIN_VALUE;
	    
	    while (true) {
		final Ticket ticketToRun;
	    
		synchronized (contents) {
		    while (true) {
			if (dead)
			    return;

			final long now = System.currentTimeMillis();
			final long timeToNextEvent;
		
			if (contents.isEmpty()) {			
			    if (kickerExitTime == Long.MIN_VALUE) {
				kickerExitTime = now + queueThreadTimeout;

				if (kickerExitTime < 0) {
				    // overflow
				    kickerExitTime = Long.MAX_VALUE;
				} 
			    }

			    // Since contents is empty the next event is exit
			    timeToNextEvent = kickerExitTime - now;

			    if (timeToNextEvent <= 0) {
				// been idle long enough, depart

				/* $$$ Do this in a finally block for the run?
				 * so no mater how this thread ends kickerThread
				 * get set to null?
				 */
				kickerThread = null;	    

				logger.log(Level.FINEST,
					   "stopping queue's thread");
				return;
			    }
			} else { // contents is non-empty
			    kickerExitTime = Long.MIN_VALUE;
			    timeToNextEvent = head.when - now;

			    if (timeToNextEvent <= 0) { 
				// The head's time has come, consume and
				// break out of inner loop to run it.

				ticketToRun = head;
				contents.remove(head);
				checkHead();
				break;
			    }
			}

			if (logger.isLoggable(Level.FINEST)) {
			    logger.log(Level.FINEST, "timeToNextEvent:{0}",
				   (timeToNextEvent == Long.MAX_VALUE ?
				    "Long.MAX_VALUE" : 
				    Long.toString(timeToNextEvent)));
			}
			
			assert timeToNextEvent > 0;

			try {
			    contents.wait(timeToNextEvent);
			} catch (InterruptedException e) {
			    /* This should never happen, our thread is
			     * private to WakeupManager and tasks
			     * calling Thread.currentThread().interrupt() is
			     * decidedly anti-social. Log, but keep on
			     * going.
			     */

			    try {				
				logger.log(Level.WARNING, 
					   "Attempt to interrupt Queue's thread");
			    } catch (Throwable t) {
				// ignore
			    }

			    /* This loop already deals with wait returning
			     * early for no reason, so going to the top
			     * of the loop is ok here - if there are no
			     * new tasks and we are not dead we will 
			     * just calc a new value for timeToNextEvent
			     */
			}

			/* Something has changed or the time has arived
			 * for action, don't know which, go back to the 
			 * the top of the inner loop to figure out what to
			 * do next
			 */
		    }
		}

		// Run the task outside of the lock
		if (ticketToRun.desc == null) {
		    // ... in this thread
		    try {
			ticketToRun.task.run();
		    } catch (Throwable e) {
			try {
			    logger.log(Level.WARNING, "Runnable.run exception", e);
			} catch (Throwable t) {
			    // don't let a problem in logging kill the thread
			}
		    }
		} else {
		    // ... in its own thread
		    try {
			ticketToRun.desc.thread(ticketToRun.task).start();
		    } catch (Throwable t) {
			try {
			    logger.log(Level.WARNING, 
				       "task thread creation exception", t);
			} catch (Throwable tt) {
			    // don't let a problem in logging kill the thread
			}			
		    }
		}
	    }
	}
    }
}


