/*
 * Runs multiple jobs in parallel
 * Copyright (C) 2004-2005 Matt Conway
 * http://simplygenius.com/
 * Copyright (C) 2005 Stephen Ostermiller
 * http://ostermiller.org/contact.pl?regarding=Java+Utilities
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * See COPYING.TXT for details.
 */

package com.Ostermiller.util;

import java.util.*;

/**
 * Runs multiple jobs in parallel, n threads at a time, and waits
 * until all threads are complete before continuing.
 * <p>
 * Typically, Parallelizer would be used to run each of the items-
 * in a for loop at the same time.  For example the following for
 * loop:
 * <pre>
 * for (int i=0; i<10; i++){
 *    System.out.println("Hello World " + i);
 * }
 * System.out.println("done");
 * </pre>
 * To this:
 * <pre>
 * Parallelizer pll = new Parallelizer();
 * for (int i=0; i<10; i++){
 *     final int j = i;
 *     pll.run(
 *         new Runnable(){
 *             System.out.println("Hello World " + j);
 *         }
 *     );
 * }
 * pll.join();
 * System.out.println("done");
 *
 * More information about this class is available from <a target="_top" href=
 * "http://ostermiller.org/utils/Parallelizer.html">ostermiller.org</a>.
 *
 * @author Matt Conway - http://simplygenius.com/
 * @author Stephen Ostermiller - http://ostermiller.org/contact.pl?regarding=Java+Utilities
 * @since ostermillerutils 1.05.00
 */
public class Parallelizer
{
	/**
	 * Constant that may be passed concurrentThreadLimit argument
	 * of the constructor indicating that no limit should be placed
	 * on the number of threads that are allowed to run concurrently.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public static final int INFINITE_THREAD_LIMIT = 0;

	/**
	 * The number of threads that are allowed to be run concurrently.
	 * (INFINITE_THREAD_LIMIT for no limit)
	 */
	private int concurrentThreadLimit = INFINITE_THREAD_LIMIT;

	/**
	 * Create a new Parallelizer with no limit on the number
	 * of threads that will be allowed to be run concurrently.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public Parallelizer(){
		this(INFINITE_THREAD_LIMIT);
	}

	/**
	 * Create a new Parallelizer with the specified limit on the number
	 * of threads that will be allowed to be run concurrently.
	 * <p>
	 * When the concurrent thread limit is reached and the parallelizer
	 * gets a new thread to run, the new thread will be queued until
	 * a thread finishes.
	 *
	 * @param concurrentThreadLimit number of threads that will be allowed
	 *     to run simultaneously or INFINITE_THREAD_LIMIT for no limit.
	 * @throws IllegalArgumentException if concurrentThreadLimit not a whole
	 *     number or INFINITE_THREAD_LIMIT
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public Parallelizer(int concurrentThreadLimit){
		if (concurrentThreadLimit < INFINITE_THREAD_LIMIT) throw new IllegalArgumentException("Bad concurrent thread limit: " + concurrentThreadLimit);
		this.concurrentThreadLimit = concurrentThreadLimit;
	}

	/**
	 * A Set of threads that are currently running.
	 * This set is also used as a lock to synchronize
	 * anything that touches running threads.
	 */
	private HashSet<Thread> runningThreads = new HashSet<Thread>();

	/**
	 * A queue of jobs that have not yet been started.
	 */
	private LinkedList<Thread> toRunQueue = new LinkedList<Thread>();

	/**
	 * Run the given job.  The given job is either run
	 * immediately or if the max number of concurrent jobs are already
	 * running, it is queued to be run when some job is finished.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @param job job which is to be run in parallel with other jobs.
	 * @throws Error if any thread that is already running has thrown an Error.
	 * @throws NullPointerException if job is null.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void run(Runnable job){
		run(null, job, null, 0);
	}

	/**
	 * Run the given job.  The given job is either run
	 * immediately or if the max number of concurrent jobs are already
	 * running, it is queued to be run when some job is finished.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @param job job which is to be run in parallel with other jobs.
	 * @param threadName name for the thread that will be created to run the job (null for auto generated thread name)
	 * @throws Error if any thread that is already running has thrown an Error.
	 * @throws NullPointerException if job is null.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void run(Runnable job, String threadName){
		run(null, job, threadName, 0);
	}

	/**
	 * Run the given job.  The given job is either run
	 * immediately or if the max number of concurrent jobs are already
	 * running, it is queued to be run when some job is finished.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @param threadGroup group in which this job should be run (null for default group).
	 * @param job job which is to be run in parallel with other jobs.
	 * @throws Error if any thread that is already running has thrown an Error.
	 * @throws NullPointerException if job is null.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void run(ThreadGroup threadGroup, Runnable job){
		run(threadGroup, job, null, 0);
	}

	/**
	 * Run the given job.  The given job is either run
	 * immediately or if the max number of concurrent jobs are already
	 * running, it is queued to be run when some job is finished.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @param threadGroup group in which this job should be run (null for default group).
	 * @param job job which is to be run in parallel with other jobs.
	 * @param threadName name for the thread that will be created to run the job (null for auto generated thread name)
	 * @throws Error if any thread that is already running has thrown an Error.
	 * @throws NullPointerException if job is null.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void run(ThreadGroup threadGroup, Runnable job, String threadName){
		run(threadGroup, job, threadName, 0);
	}

	/**
	 * Run the given job.  The given job is either run
	 * immediately or if the max number of concurrent jobs are already
	 * running, it is queued to be run when some job is finished.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @param threadGroup group in which this job should be run (null for default group).
	 * @param job job which is to be run in parallel with other jobs.
	 * @param threadName name for the thread that will be created to run the job (null for auto generated thread name)
	 * @param stackSize system dependent stack size suggestion for thread creation (0 for default stack size).
	 * @throws Error if any thread that is already running has thrown an Error.
	 * @throws NullPointerException if job is null.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void run(ThreadGroup threadGroup, final Runnable job, String threadName, long stackSize){
		throwFirstError();

		Runnable jobWrapper = new Runnable(){
			public void run(){
				try {
					job.run();
				} catch (RuntimeException runtimeException){
					// Put exceptions in the exception queue
					synchronized(runningThreads){
						exceptionList.add(runtimeException);
					}
				} catch (Error error){
					// Put errors in the error queue
					synchronized(runningThreads){
						errorList.add(error);
					}
				} finally {
					synchronized(runningThreads){
						// when done remove ourselves from the list
						// of running threads.
						runningThreads.remove(Thread.currentThread());
						// Notify the block method.
						runningThreads.notifyAll();
					}
					// If there are jobs queued up to be run, now would
					// be a good time to run them.
					startAJobIfNeeded();
				}
			}
		};

		// ensure the thread name is not null, and auto generate a name if it is
		threadName = getNextThreadName(threadName);

		// If we are already running the max number of jobs, queue this job up
		synchronized(runningThreads){
			toRunQueue.add(
				new Thread(
					threadGroup,
					jobWrapper,
					threadName,
					stackSize
				)
			);
		}

		// Now that the job is in the queue of jobs to run,
		// check the queue and see if the job should be started
		startAJobIfNeeded();
	}

	/**
	 * An number to assign to the next auto generated thread name
	 */
	private static int threadNameCount = 0;

	/**
	 * Ensure the given thread name is not null.  If not null, return it,
	 * if it is null, then then generate a name.
	 *
	 * @param threadName existing thread name to check
	 * @return the given thread name or a generated thread name if the specified name was null.
	 */
	private static String getNextThreadName(String threadName){
		if (threadName != null) return threadName;
		return "Parallelizer-"+(threadNameCount++);
	}

	/**
	 * A queue of exceptions that running threads have thrown.
	 */
	private LinkedList<RuntimeException> exceptionList = new LinkedList<RuntimeException>();

	/**
	 * Remove the first exception from the exception list and throw it.
	 *
	 * @throws RuntimeException if a running thread has thrown an exception not yet thrown by this method.
	 */
	private void throwFirstException(){
		synchronized(runningThreads){
			if (exceptionList.size() > 0){
				throw (RuntimeException)exceptionList.removeFirst();
			}
		}
	}

	/**
	 * A queue of exceptions that running threads have thrown.
	 */
	private LinkedList<Error> errorList = new LinkedList<Error>();

	/**
	 * Remove the first error from the error list and throw it.
	 *
	 * @throws Error if a running thread has thrown an error not yet thrown by this method.
	 */
	private void throwFirstError() throws Error {
		synchronized(runningThreads){
			if (errorList.size() > 0){
				throw (Error)errorList.removeFirst();
			}
		}
	}

	/**
	 * Remove a job from the toRunQueue, create a thread for it,
	 * start the thread, and put the job in the set of running jobs.
	 * But do all this only if there are jobs queued up to be run
	 * and we are not already running the max number of concurrent
	 * jobs at once.
	 */
	private void startAJobIfNeeded(){
		synchronized(runningThreads){
			// If we are already running the max number of jobs, just return
			if (concurrentThreadLimit != INFINITE_THREAD_LIMIT){
				if (runningThreads.size() >= concurrentThreadLimit) return;
			}

			// If there are no more job to run, return
			if (toRunQueue.size() == 0) return;

			// Get a job out of the queue
			Thread thread = (Thread)toRunQueue.removeFirst();

			// Put the thread in the list of running threads
			runningThreads.add(thread);
			thread.start();
		}
	}

	/**
	 * Return true iff all jobs that have been requested to run
	 * in this Parallelizer have completed.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @return Whether all jobs are done or not.
	 * @throws Error if any of the running threads has thrown an Error.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public boolean done(){
		throwFirstError();
		synchronized(runningThreads){
			return (toRunQueue.size() + runningThreads.size()) == 0;
		}
	}

	/**
	 * All currently running threads will be interrupted.
	 * The threads interrupted threads may die, causing
	 * jobs that were queued but not yet started, to start.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @throws Error if any of the running threads has thrown an Error.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void interrupt(){
		throwFirstError();
		synchronized(runningThreads){
			for (Iterator<Thread> i=runningThreads.iterator(); i.hasNext();){
				((Thread)i.next()).interrupt();
				throwFirstError();
			}
		}
	}

	/**
	 * Dump the stack of each running thread.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @throws Error if any of the running threads has thrown an Error.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void dumpStack(){
		throwFirstError();
		synchronized(runningThreads){
			for (Iterator<Thread> i=runningThreads.iterator(); i.hasNext();){
				((Thread)i.next()).dumpStack();
				throwFirstError();
			}
		}
	}

	/**
	 * Gets a list of all running threads.  There may be jobs that
	 * are queued and do not yet have threads.  These job are not
	 * returned.
	 * <p>
	 * If this method throws an error, that
	 * error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the error.
	 *
	 * @throws Error if any of the running threads has thrown an Error.
	 * @return an array of all currently running threads.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public Thread[] getRunningThreads(){
		throwFirstError();
		synchronized(runningThreads){
			return (Thread[])runningThreads.toArray(new Thread[0]);
		}
	}

	/**
	 * Block until all the jobs in this Parallelizer have run
	 * and then return.
	 * <p>
	 * If this method throws an exception or an error, that
	 * exception or error may be handled and this method
	 * may be called again as it will not rethrow the same
	 * instance of the exception or error.
	 *
	 * @throws InterruptedException if interrupted while waiting.
	 * @throws RuntimeException any running thread throws or has thrown a runtime exception.
	 * @throws Error if any of the running threads throws or has thrown an Error.
	 *
	 * @since ostermillerutils 1.05.00
	 */
	public void join() throws InterruptedException {
		while (!done()){
			synchronized(runningThreads){
				throwFirstException();
				runningThreads.wait();
				throwFirstError();
				throwFirstException();
			}
		}
	}
}
