package teetime.framework;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import teetime.framework.exceptionHandling.IgnoringStageListener;
import teetime.framework.exceptionHandling.StageExceptionHandler;
import teetime.framework.signal.ValidatingSignal;
import teetime.framework.validation.AnalysisNotValidException;
import teetime.util.Pair;

/**
 * Represents an Analysis to which stages can be added and executed later.
 * This needs a {@link AnalysisConfiguration},
 * in which the adding and configuring of stages takes place.
 * To start the analysis {@link #execute()} needs to be executed.
 * This class will automatically create threads and join them without any further commitment.
 */
public class Analysis implements UncaughtExceptionHandler {

	private static final Logger LOGGER = LoggerFactory.getLogger(Analysis.class);

	private final AnalysisConfiguration configuration;

	private final StageExceptionHandler listener;

	private boolean executionInterrupted = false;

	private final List<Thread> consumerThreads = new LinkedList<Thread>();
	private final List<Thread> finiteProducerThreads = new LinkedList<Thread>();
	private final List<Thread> infiniteProducerThreads = new LinkedList<Thread>();

	private final Collection<Pair<Thread, Throwable>> exceptions = new ConcurrentLinkedQueue<Pair<Thread, Throwable>>();

	private boolean initialized;

	/**
	 * Creates a new {@link Analysis} that skips validating the port connections and uses the default listener.
	 *
	 * @param configuration
	 *            to be used for the analysis
	 */
	public Analysis(final AnalysisConfiguration configuration) {
		this(configuration, false, new IgnoringStageListener());
	}

	public Analysis(final AnalysisConfiguration configuration, final boolean validationEnabled) {
		this(configuration, validationEnabled, new IgnoringStageListener());
	}

	/**
	 * Creates a new {@link Analysis} that skips validating the port connections and uses a specific listener.
	 *
	 * @param configuration
	 *            to be used for the analysis
	 * @param listener
	 *            specific listener for the exception handling
	 */
	public Analysis(final AnalysisConfiguration configuration, final StageExceptionHandler listener) {
		this(configuration, false, listener);
	}

	public Analysis(final AnalysisConfiguration configuration, final boolean validationEnabled, final StageExceptionHandler listener) {
		this.configuration = configuration;
		this.listener = listener;
		if (validationEnabled) {
			validateStages();
		}
		init();
	}

	private void validateStages() {
		// BETTER validate concurrently
		final List<Stage> threadableStageJobs = this.configuration.getThreadableStageJobs();
		for (Stage stage : threadableStageJobs) {
			// // portConnectionValidator.validate(stage);
			// }

			final ValidatingSignal validatingSignal = new ValidatingSignal();
			stage.onSignal(validatingSignal, null);
			if (validatingSignal.getInvalidPortConnections().size() > 0) {
				throw new AnalysisNotValidException(validatingSignal.getInvalidPortConnections());
			}
		}
	}

	/**
	 * This initializes Analysis and needs to be run right before starting it.
	 *
	 * @deprecated As of release 1.1. Will be no longer needed, as functionality is moved into the constructor.
	 */
	@Deprecated
	public void init() {
		if (initialized) {
			return;
		}
		initialized = true;

		final List<Stage> threadableStageJobs = this.configuration.getThreadableStageJobs();
		for (Stage stage : threadableStageJobs) {
			StageExceptionHandler newListener;
			try {
				newListener = listener.getClass().newInstance();
			} catch (InstantiationException e) {
				throw new IllegalStateException(e);
			} catch (IllegalAccessException e) {
				throw new IllegalStateException(e);
			}
			switch (stage.getTerminationStrategy()) {
			case BY_SIGNAL: {
				RunnableConsumerStage runnable;
				if (stage instanceof AbstractConsumerStage<?>) {
					runnable = new RunnableConsumerStage(stage, ((AbstractConsumerStage<?>) stage).getIdleStrategy(), newListener); // FIXME remove this word-around
				} else {
					runnable = new RunnableConsumerStage(stage, newListener);
				}
				final Thread thread = new Thread(runnable);
				stage.setOwningThread(thread);
				this.consumerThreads.add(thread);
				break;
			}
			case BY_SELF_DECISION: {
				RunnableProducerStage runnable = new RunnableProducerStage(stage, newListener);
				final Thread thread = new Thread(runnable);
				stage.setOwningThread(thread);
				this.finiteProducerThreads.add(thread);
				break;
			}
			case BY_INTERRUPT: {
				RunnableProducerStage runnable = new RunnableProducerStage(stage, newListener);
				final Thread thread = new Thread(runnable);
				stage.setOwningThread(thread);
				this.infiniteProducerThreads.add(thread);
				break;
			}
			default:
				break;
			}
		}

	}

	/**
	 * This method will start the Analysis and all containing stages.
	 *
	 * @return a collection of thread/throwable pairs
	 *
	 * @deprecated As of release 1.1, replaced by {@link #execute()}
	 */
	@Deprecated
	public Collection<Pair<Thread, Throwable>> start() {
		// start analysis
		startThreads(this.consumerThreads);
		startThreads(this.finiteProducerThreads);
		startThreads(this.infiniteProducerThreads);

		// wait for the analysis to complete
		try {
			for (Thread thread : this.finiteProducerThreads) {
				thread.join();
			}

			for (Thread thread : this.consumerThreads) {
				thread.join();
			}
		} catch (InterruptedException e) {
			LOGGER.error("Analysis has stopped unexpectedly", e);
			for (Thread thread : this.finiteProducerThreads) {
				thread.interrupt();
			}

			for (Thread thread : this.consumerThreads) {
				thread.interrupt();
			}
		}

		for (Thread thread : this.infiniteProducerThreads) {
			thread.interrupt();
		}

		return this.exceptions;
	}

	/**
	 * This method will start the Analysis and all containing stages.
	 */
	public void execute() {
		start();
		if (!exceptions.isEmpty()) {
			throw new AnalysisException(exceptions);
		}
	}

	private void startThreads(final Iterable<Thread> threads) {
		for (Thread thread : threads) {
			thread.setUncaughtExceptionHandler(this);
			thread.start();
		}
	}

	/**
	 * Retrieves the Configuration which was used to add and arrange all stages needed for the Analysis
	 *
	 * @return Configuration used for the Analysis
	 */
	public AnalysisConfiguration getConfiguration() {
		return this.configuration;
	}

	@Override
	public void uncaughtException(final Thread thread, final Throwable throwable) {
		if (!executionInterrupted) {
			executionInterrupted = true;
			LOGGER.warn("Thread " + thread + " was interrupted. Terminating analysis now.");
			for (Stage stage : configuration.getThreadableStageJobs()) {
				if (stage.getOwningThread() != thread) {
					if (stage.getTerminationStrategy() == TerminationStrategy.BY_SELF_DECISION) {
						stage.terminate();
					}
				}
			}
		}
		this.exceptions.add(Pair.of(thread, throwable));
	}
}

@@ -2717,16 +2717,101 @@
 %09init();
+ // http://stackoverflow.com/a/19504865 -%3E we should consider making this class final
 %0A%09%7D%0A%0A%09pr

Comment with solution a solution to a PMD issue