/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s): Alexandre Iline.
 *
 * The Original Software is the Jemmy library.
 * The Initial Developer of the Original Software is Alexandre Iline.
 * All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 *
 *
 * $Id$ $Revision$ $Date$
 *
 */

package org.netbeans.jemmy;

import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.InvocationEvent;
import java.lang.reflect.InvocationTargetException;

/**
 *
 * Provides functionality to work with java.awt.EventQueue empty.
 *
 * <BR><BR>Timeouts used: <BR>
 * QueueTool.WaitQueueEmptyTimeout - timeout to wait queue emptied<BR>
 * QueueTool.QueueCheckingDelta - time delta to check result<BR>
 * QueueTool.LockTimeout - time to wait queue locked after lock action has been put there<BR>
 * QueueTool.InvocationTimeout - time for action was put into queue to be started<BR>
 * QueueTool.MaximumLockingTime - maximum time to lock queue.<br>
 *
 * @see Timeouts
 *
 * @author Alexandre Iline (alexandre.iline@sun.com)
 *
 */
public class QueueTool implements Outputable, Timeoutable {

    private final static long WAIT_QUEUE_EMPTY_TIMEOUT = 180000;
    private final static long QUEUE_CHECKING_DELTA = 10;
    private final static long LOCK_TIMEOUT = 180000;
    private final static long MAXIMUM_LOCKING_TIME = 180000;
    private final static long INVOCATION_TIMEOUT = 180000;

    private static JemmyQueue jemmyQueue = null;

    private TestOut output;
    private Timeouts timeouts;
    private Locker locker;
    private Waiter lockWaiter;

    /**
     * Constructor.
     */
    public QueueTool() {
	locker = new Locker();
	lockWaiter = new Waiter(new Waitable() {
		public Object actionProduced(Object obj) {
		    return(locker.isLocked() ? "" : null);
		}
		public String getDescription() {
		    return("Event queue to be locked");
		}
	    });
	setOutput(JemmyProperties.getProperties().getOutput());
	setTimeouts(JemmyProperties.getProperties().getTimeouts());
    }

    /**
     * Returns system EventQueue.
     * @return system EventQueue.
     */
    public static EventQueue getQueue() {
        return(Toolkit.getDefaultToolkit().getSystemEventQueue());
    }

    /**
     * Map to <code>EventQueue.isDispatchThread()</code>.
     * @return true if this thread is the AWT dispatching thread.
     */
    public static boolean isDispatchThread() {
	return(getQueue().isDispatchThread());
    }

    /**
     * Checks if system event queue is empty.
     * @return true if EventQueue is empty.
     */
    public static boolean checkEmpty() {
	return(getQueue().peekEvent() == null);
    }

    /**
     * Shortcuts event if 
     * <code>((JemmyProperties.getCurrentDispatchingModel() & JemmyProperties.SHORTCUT_MODEL_MASK) != 0)</code>
     * and if executed in the dispatch thread.
     * Otherwise posts event.
     * @param event Event to dispatch.
     */
    public static void processEvent(AWTEvent event) {
        if((JemmyProperties.getCurrentDispatchingModel() & 
            JemmyProperties.SHORTCUT_MODEL_MASK) != 0) {
            installQueue();
        }    
        if((JemmyProperties.getCurrentDispatchingModel() & 
            JemmyProperties.SHORTCUT_MODEL_MASK) != 0 &&
           isDispatchThread()) {
            shortcutEvent(event);
        } else {
            postEvent(event);
        }
    }

    /**
     * Simply posts events into the system event queue.
     * @param event Event to dispatch.
     */
    public static void postEvent(AWTEvent event) {
        getQueue().postEvent(event);
    }

    /**
     * Dispatches event ahead of all events staying in the event queue.
     * @param event an event to be shortcut.
     */
    public static void shortcutEvent(AWTEvent event) {
        installQueue();
        jemmyQueue.shortcutEvent(event);
    }

    /**
     * Installs own Jemmy EventQueue implementation. 
     * The method is executed in dispatchmode only.
     * @see #uninstallQueue
     */
    public static void installQueue() {
        if(jemmyQueue == null) {
            jemmyQueue = new JemmyQueue();
        }
        jemmyQueue.install();
    }

    /**
     * Uninstalls own Jemmy EventQueue implementation. 
     * @see #installQueue
     */
    public static void uninstallQueue() {
        if(jemmyQueue != null) {
            jemmyQueue.uninstall();
        }
    }

    static {
	Timeouts.initDefault("QueueTool.WaitQueueEmptyTimeout", WAIT_QUEUE_EMPTY_TIMEOUT);
	Timeouts.initDefault("QueueTool.QueueCheckingDelta", QUEUE_CHECKING_DELTA);
	Timeouts.initDefault("QueueTool.LockTimeout", LOCK_TIMEOUT);
	Timeouts.initDefault("QueueTool.InvocationTimeout", INVOCATION_TIMEOUT);
	Timeouts.initDefault("QueueTool.MaximumLockingTime", MAXIMUM_LOCKING_TIME);
    }

    /**
     * Defines current timeouts.
     * 
     * @param	ts ?t? A collection of timeout assignments.
     * @see	org.netbeans.jemmy.Timeouts
     * @see	org.netbeans.jemmy.Timeoutable
     * @see #getTimeouts
     */
    public void setTimeouts(Timeouts ts) {
	timeouts = ts;
	lockWaiter.setTimeouts(getTimeouts().cloneThis());
    }

    /**
     * Return current timeouts.
     * @return the collection of current timeout assignments.
     * @see org.netbeans.jemmy.Timeouts
     * @see org.netbeans.jemmy.Timeoutable
     * @see #setTimeouts
     */
    public Timeouts getTimeouts() {
	return(timeouts);
    }

    /**
     * Defines print output streams or writers.
     * @param out Identify the streams or writers used for print output.
     * @see org.netbeans.jemmy.Outputable
     * @see org.netbeans.jemmy.TestOut
     * @see #getOutput
     */
    public void setOutput(TestOut out) {
	output = out;
	lockWaiter.setOutput(output.createErrorOutput());
    }

    /**
     * Returns print output streams or writers.
     * @return an object that contains references to objects for
     * printing to output and err streams.
     * @see org.netbeans.jemmy.Outputable
     * @see org.netbeans.jemmy.TestOut
     * @see #setOutput
     */
    public TestOut getOutput() {
	return(output);
    }

    /**
     * Waits for system event queue empty.
     * Uses "QueueTool.WaitQueueEmptyTimeout" milliseconds to wait.
     * @throws TimeoutExpiredException
     */
    public void waitEmpty() {
	Waiter waiter = new Waiter(new Waitable() {
	    public Object actionProduced(Object obj) {
		if(checkEmpty()) {
		    return("Empty");
		}
		return(null);
	    }
	    public String getDescription() {
		return("Wait event queue empty");
	    }
	});
	waiter.setTimeoutsToCloneOf(timeouts, "QueueTool.WaitQueueEmptyTimeout");
	waiter.setOutput(output);
	try {
	    waiter.waitAction(null);
	} catch(TimeoutExpiredException e) {
            final AWTEvent event = getQueue().peekEvent();
            // if event != null run toString in dispatch thread
            String eventToString = (event == null) ? "null" : (String)invokeSmoothly(
                new QueueTool.QueueAction("event.toString()") {
                    public Object launch() {
                        return event.toString();
                    }
                }
            );
            getOutput().printErrLine("Event at the top of stack: " + eventToString);
            throw(e);
	} catch(InterruptedException e) {
	    output.printStackTrace(e);
	}
    }

    /**
     * Waits for system event queue be empty for <code>emptyTime</code> milliseconds.
     * Uses "QueueTool.WaitQueueEmptyTimeout" milliseconds to wait.
     * 
     * @param	emptyTime time for the queue to stay empty.
     * @throws	TimeoutExpiredException
     */
    public void waitEmpty(long emptyTime) {

	StayingEmptyWaiter waiter = new StayingEmptyWaiter(emptyTime);
	waiter.setTimeoutsToCloneOf(timeouts, "QueueTool.WaitQueueEmptyTimeout");
	waiter.setOutput(output);
	try {
	    waiter.waitAction(null);
	} catch(TimeoutExpiredException e) {
            final AWTEvent event = getQueue().peekEvent();
            String eventToString = (event == null) ? "null" : (String)invokeSmoothly(
            new QueueTool.QueueAction("event.toString()") {
                public Object launch() {
                    return event.toString();
                }
            }
            );
            getOutput().printErrLine("Event at the top of stack: " + eventToString);
            throw(e);
	} catch(InterruptedException e) {
	    output.printStackTrace(e);
	}
    }

    /**
     * Invokes action through EventQueue.
     * Does not wait for it execution.
     * @param action an action to be invoked.
     */
    public void invoke(QueueAction action) {
	output.printTrace("Invoking \"" + action.getDescription() + "\" action through event queue");
	EventQueue.invokeLater(action);
    }

    /**
     * Invokes runnable through EventQueue.
     * Does not wait for it execution.
     * @param	runnable a runnable to be invoked.
     * @return	QueueAction instance which can be use for execution monitoring.
     * @see	QueueTool.QueueAction
     */
    public QueueAction invoke(Runnable runnable) {
	QueueAction result = new RunnableRunnable(runnable);
	invoke(result);
	return(result);
    }

    /**
     * Invokes action through EventQueue.
     * Does not wait for it execution.
     * @param action an action to be invoked.
     * @param param <code>action.launch(Object)</code> method parameter.
     * @return QueueAction instance which can be use for execution monitoring.
     * @see QueueTool.QueueAction
     */
    public QueueAction invoke(Action action, Object param) {
	QueueAction result = new ActionRunnable(action, param);
	invoke(result);
	return(result);
    }

    /**
     * Being executed outside of AWT dispatching thread, 
     * invokes an action through the event queue.
     * Otherwise executes <code>action.launch()</code> method
     * directly.
     * @param action anaction to be executed.
     * @return Action result.
     */
    public Object invokeSmoothly(QueueAction action) {
        if(!getQueue().isDispatchThread()) {
            return(invokeAndWait(action));
        } else {
            try {
                return(action.launch());
            } catch(Exception e) {
                throw(new JemmyException("Exception in " + action.getDescription(), e));
            }
        }
    }

    /**
     * Being executed outside of AWT dispatching thread, 
     * invokes a runnable through the event queue.
     * Otherwise executes <code>runnable.run()</code> method
     * directly.
     * @param runnable a runnable to be executed.
     */
    public void invokeSmoothly(Runnable runnable) {
        if(!getQueue().isDispatchThread()) {
            invokeAndWait(runnable);
        } else {
            runnable.run();
        }
    }

    /**
     * Being executed outside of AWT dispatching thread, 
     * invokes an action through the event queue.
     * Otherwise executes <code>action.launch(Object)</code> method
     * directly.
     * @param action anaction to be executed.
     * @param param an action parameter
     * @return Action result.
     */
    public Object invokeSmoothly(Action action, Object param) {
        if(!getQueue().isDispatchThread()) {
            return(invokeAndWait(action, param));
        } else {
            return(action.launch(param));
        }
    }

    /**
     * Invokes action through EventQueue.
     * Waits for it execution.
     * @param action an action to be invoked.
     * @return a result of action
     * @throws TimeoutExpiredException if action
     * was not executed in "QueueTool.InvocationTimeout" milliseconds.
     */
    public Object invokeAndWait(QueueAction action) {
	
        class JemmyInvocationLock {}
        Object lock = new JemmyInvocationLock();
        InvocationEvent event = 
            new InvocationEvent(Toolkit.getDefaultToolkit(), 
                                action, 
                                lock,
				true);
	try {
            synchronized (lock) {
                getQueue().postEvent(event);
                lock.wait();
            }
	} catch(InterruptedException e) {
	    throw(new JemmyException("InterruptedException during " + 
				     action.getDescription() + 
				     " execution", e));
	}
	if(action.getException() != null) {
	    throw(new JemmyException("Exception in " + action.getDescription(), 
				     action.getException()));
	}
	if(event.getException() != null) {
	    throw(new JemmyException("Exception in " + action.getDescription(), 
				     event.getException()));
	}
	return(action.getResult());
    }

    /**
     * Invokes runnable through EventQueue.
     * Waits for it execution.
     * @param runnable a runnable to be invoked.
     * @throws	TimeoutExpiredException if runnable
     * was not executed in "QueueTool.InvocationTimeout" milliseconds.
     */
    public void invokeAndWait(Runnable runnable) {
	invokeAndWait(new RunnableRunnable(runnable));
    }

    /**
     * Invokes action through EventQueue.
     * Waits for it execution.
     * May throw TimeoutExpiredException if action
     * was not executed in "QueueTool.InvocationTimeout" milliseconds.
     * @param action an action to be invoked.
     * @param	param action.launch(Object method parameter.
     * @return a result of action
     * @throws	TimeoutExpiredException if action
     * was not executed in "QueueTool.InvocationTimeout" milliseconds.
     */
    public Object invokeAndWait(Action action, Object param) {
	return(invokeAndWait(new ActionRunnable(action, param)));
    }

    /**
     * Locks EventQueue.
     * Locking will be automatically aborted after 
     * "QueueTool.MaximumLockingTime" milliseconds.
     * @see #unlock()
     * @throws TimeoutExpiredException
     */
    public void lock() {
	output.printTrace("Locking queue.");
	invoke(locker);
	try {
	    lockWaiter.
		getTimeouts().
		setTimeout("Waiter.WaitingTime",
			   timeouts.
			   getTimeout("QueueTool.LockTimeout"));
	    lockWaiter.
		getTimeouts().
		setTimeout("Waiter.TimeDelta",
			   timeouts.
			   getTimeout("QueueTool.QueueCheckingDelta"));
	    lockWaiter.waitAction(null);
	} catch(InterruptedException e) {
	    output.printStackTrace(e);
	}
    }

    /**
     * Unlocks EventQueue.
     * @see #lock()
     */
    public void unlock() {
	output.printTrace("Unlocking queue.");
	locker.setLocked(false);
    }

    /**
     * Locks event queue for "time" milliseconds.
     * Returns immediately after locking.
     * @param	time a time to lock the queue for.
     */
    public void lock(long time) {
	output.printTrace("Locking queue for " + Long.toString(time) + " milliseconds");
	lock();
	invoke(new UnlockPostponer(time));
    }

    /**
     * Sais if last locking was expired.
     * @return true if last locking had beed expired.
     */
    public boolean wasLockingExpired() {
	return(locker.expired);
    }

    /**
     * Action to be excuted through event queue.
     * Even if it was executed without waiting by <code>invoke(QueueAction)</code>
     * execution process can be monitored by <code>getResult()</code>,
     * <code>getException()</code>, <code>getFinished()</code> methods.
     */
    public static abstract class QueueAction implements Runnable {
	private boolean finished;
	private Exception exception;
	private Object result;
	private String description;
	/**
	 * Constructor.
         * @param description a description.
	 */
	public QueueAction(String description) {
	    this.description = description;
	    finished = false;
	    exception = null;
	    result = null;
        }
	/**
	 * Method to implement action functionality.
         * @return an Object - action result
         * @throws Exception
	 */
	public abstract Object launch() 
	    throws Exception;

	/**
	 */
	public final void run() {
	    finished = false;
	    exception = null;
	    result = null;
	    try {
		result = launch();
	    } catch(Exception e) {
		exception = e;
	    }
	    finished = true;
	}
	/**
	 * Action description.
         * @return the description.
	 */
	public String getDescription() {
	    return(description);
	}
	/**
	 * Returns action result if action has already been finished,
	 * null otherwise.
         * @return an action result.
	 */
	public Object getResult() {
	    return(result);
	}
	/**
	 * Returns exception occured during action execution (if any).
         * @return the Exception happened inside <code>launch()</code> method.
	 */
	public Exception getException() {
	    return(exception);
	}
	/**
	 * Informs whether action has been finished or not.
         * @return true if this action have been finished
	 */
	public boolean getFinished() {
	    return(finished);
	}
    }

    private static class JemmyQueue extends EventQueue {
        private boolean installed = false;
        public void shortcutEvent(AWTEvent event) {
            super.dispatchEvent(event);
        }
        protected void dispatchEvent(AWTEvent event) {
            //it's necessary to catch exception here.
            //because test might already fail by timeout
            //but generated events are still in stack
            try {
                super.dispatchEvent(event);
            } catch(Exception e) {
                //the exceptions should be printed into
                //Jemmy output - not System.out
                JemmyProperties.getCurrentOutput().printStackTrace(e);
            }
        }
        public synchronized void install() {
            if(!installed) {
                getQueue().push(this);
                installed = true;
            }
        }
        public synchronized void uninstall() {
            if(installed) {
                pop();
                installed = false;
            }
        }
    }

    private class EventWaiter implements Runnable {
	boolean empty = true;
	long emptyTime;
	public EventWaiter(long emptyTime) {
	    this.emptyTime = emptyTime;
	}
	public void run() {
	    long startTime = System.currentTimeMillis();
	    while((empty = checkEmpty()) && 
		  (System.currentTimeMillis() - startTime) < emptyTime) {
		timeouts.sleep("QueueTool.QueueCheckingDelta");
	    }
	}
    }

    private class StayingEmptyWaiter extends Waiter {
	long emptyTime;

	public StayingEmptyWaiter(long emptyTime) {
	    this.emptyTime = emptyTime;
	}

	public Object actionProduced(Object obj) {
	    try {
		EventWaiter eventWaiter = new EventWaiter(emptyTime);
		EventQueue.invokeAndWait(eventWaiter);
		if(eventWaiter.empty &&
		   timeFromStart() <= super.getTimeouts().getTimeout("Waiter.WaitingTime")) {
		    return("Reached");
		}
	    } catch(InterruptedException e) {
		output.printStackTrace(e);
	    } catch(InvocationTargetException e) {
		output.printStackTrace(e);
	    }
	    return(null);
	}
	public String getDescription() {
	    return("Wait event queue staying empty for " + 
		   Long.toString(emptyTime));
	}
    }

    private class ActionRunnable extends QueueAction {
	Action action;
	Object param;
	public ActionRunnable(Action action, Object param) {
	    super(action.getDescription());
	    this.action = action;
	    this.param = param;
	}
	public Object launch() throws Exception {
	    return(action.launch(param)); 
	}
    }

    private class RunnableRunnable extends QueueAction {
	Runnable action;
	public RunnableRunnable(Runnable action) {
	    super("Runnable");
	    this.action = action;
	}
	public Object launch() throws Exception {
	    action.run();
	    return(null);
	}
    }

    private class Locker extends QueueAction {
	boolean locked = false;
	long wholeTime, deltaTime;
	boolean expired;
	public Locker() {
	    super("Event queue locking");
	}
	public Object launch() {
	    wholeTime = timeouts.getTimeout("QueueTool.MaximumLockingTime");
	    deltaTime = timeouts.getTimeout("QueueTool.QueueCheckingDelta");
	    setLocked(true);
	    expired = false;
	    long startTime = System.currentTimeMillis();
	    while(isLocked()) {
		try {
		    Thread.sleep(deltaTime);
		} catch(InterruptedException e) {
		    getOutput().printStackTrace(e);
		}
		if(System.currentTimeMillis() - startTime > wholeTime) {
		    getOutput().printLine("Locking has been expired!");
		    expired = true;
		    break;
		}
	    }
	    return(null);
	}
	public void setLocked(boolean locked) {
	    this.locked = locked;
	}
	public boolean isLocked() {
	    return(locked);
	}
    }

    private class UnlockPostponer implements Runnable {
	long time;
	public UnlockPostponer(long time) {
	    this.time = time;
	}
	public void run() {
	    new Timeout("", time).sleep();
	    unlock();
	}
    }
}
