// GPars - Groovy Parallel Systems
//
// Copyright © 2008-2012  The original author or authors
//
// Licensed 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 groovyx.gpars.group;

import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.actor.Actor;
import groovyx.gpars.actor.BlockingActor;
import groovyx.gpars.actor.DefaultActor;
import groovyx.gpars.actor.DynamicDispatchActor;
import groovyx.gpars.actor.ReactiveActor;
import groovyx.gpars.actor.StaticDispatchActor;
import groovyx.gpars.actor.impl.RunnableBackedBlockingActor;
import groovyx.gpars.agent.Agent;
import groovyx.gpars.dataflow.Dataflow;
import groovyx.gpars.dataflow.DataflowReadChannel;
import groovyx.gpars.dataflow.DataflowVariable;
import groovyx.gpars.dataflow.DataflowWriteChannel;
import groovyx.gpars.dataflow.LazyDataflowVariable;
import groovyx.gpars.dataflow.Promise;
import groovyx.gpars.dataflow.Select;
import groovyx.gpars.dataflow.SelectableChannel;
import groovyx.gpars.dataflow.operator.DataflowOperator;
import groovyx.gpars.dataflow.operator.DataflowPrioritySelector;
import groovyx.gpars.dataflow.operator.DataflowProcessor;
import groovyx.gpars.dataflow.operator.DataflowProcessorAtomicBoundAllClosure;
import groovyx.gpars.dataflow.operator.DataflowSelector;
import groovyx.gpars.scheduler.Pool;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import static java.util.Arrays.asList;

/**
 * Provides a common super class of pooled parallel groups.
 *
 * @author Vaclav Pech, Alex Tkachman
 *         Date: May 8, 2009
 */
@SuppressWarnings({"RawUseOfParameterizedType"})
public abstract class PGroup {

    protected static final String A_SPLITTER_NEEDS_AN_INPUT_CHANNEL_AND_AT_LEAST_ONE_OUTPUT_CHANNEL_TO_BE_CREATED = "A splitter needs an input channel and at least one output channel to be created.";

    /**
     * Stores the group actors' thread pool
     */
    private final Pool threadPool;

    public Pool getThreadPool() {
        return threadPool;
    }

    /**
     * Creates a group for actors, agents, tasks and operators. The actors will share a common daemon thread pool.
     *
     * @param threadPool The thread pool to use by the group
     */
    protected PGroup(final Pool threadPool) {
        this.threadPool = threadPool;
    }

    /**
     * Creates a new instance of DefaultActor, using the passed-in runnable/closure as the body of the actor's act() method.
     * The created actor will belong to the pooled parallel group.
     *
     * @param handler The body of the newly created actor's act method.
     * @return A newly created instance of the DefaultActor class
     */
    public final DefaultActor actor(@DelegatesTo(DefaultActor.class) final Runnable handler) {
        final DefaultActor actor = new DefaultActor(handler);
        actor.setParallelGroup(this);
        actor.start();
        return actor;
    }

    /**
     * Creates a new instance of BlockingActor, using the passed-in closure as the body of the actor's act() method.
     * The created actor will be part of the default actor group.
     *
     * @param handler The body of the newly created actor's act method.
     * @return A newly created instance of the BlockingActor class
     */
    public final BlockingActor blockingActor(@DelegatesTo(BlockingActor.class) final Runnable handler) {
        final BlockingActor actor = new RunnableBackedBlockingActor(handler);
        actor.setParallelGroup(this);
        actor.start();
        return actor;
    }

    /**
     * Creates a new instance of DefaultActor, using the passed-in runnable/closure as the body of the actor's act() method.
     * The actor will cooperate in thread sharing with other actors sharing the same thread pool in a fair manner.
     * The created actor will belong to the pooled parallel group.
     *
     * @param handler The body of the newly created actor's act method.
     * @return A newly created instance of the DefaultActor class
     */
    public final DefaultActor fairActor(@DelegatesTo(DefaultActor.class) final Runnable handler) {
        final DefaultActor actor = new DefaultActor(handler);
        actor.setParallelGroup(this);
        actor.makeFair();
        actor.start();
        return actor;
    }

    /**
     * Creates a reactor around the supplied code.
     * When a reactor receives a message, the supplied block of code is run with the message
     * as a parameter and the result of the code is send in reply.
     *
     * @param code The code to invoke for each received message
     * @return A new instance of ReactiveEventBasedThread
     */
    public final Actor reactor(@DelegatesTo(Actor.class) final Closure code) {
        final Actor actor = new ReactiveActor(code);
        actor.setParallelGroup(this);
        actor.start();
        return actor;
    }

    /**
     * Creates a reactor around the supplied code, which will cooperate in thread sharing with other actors sharing the same thread pool
     * When a reactor receives a message, the supplied block of code is run with the message
     * as a parameter and the result of the code is send in reply.
     *
     * @param code The code to invoke for each received message
     * @return A new instance of ReactiveEventBasedThread
     */
    public final Actor fairReactor(@DelegatesTo(Actor.class) final Closure code) {
        final ReactiveActor actor = new ReactiveActor(code);
        actor.setParallelGroup(this);
        actor.makeFair();
        actor.start();
        return actor;
    }

    /**
     * Creates an instance of DynamicDispatchActor.
     *
     * @param code The closure specifying individual message handlers.
     * @return The new started actor
     */
    public final Actor messageHandler(@DelegatesTo(Actor.class) final Closure code) {
        final DynamicDispatchActor actor = new DynamicDispatchActor().become(code);
        actor.setParallelGroup(this);
        actor.start();
        return actor;
    }

    /**
     * Creates an instance of DynamicDispatchActor, which will cooperate in thread sharing with other actors sharing the same thread pool
     *
     * @param code The closure specifying individual message handlers.
     * @return The new started actor
     */
    public final Actor fairMessageHandler(@DelegatesTo(Actor.class) final Closure code) {
        final DynamicDispatchActor actor = new DynamicDispatchActor().become(code);
        actor.setParallelGroup(this);
        actor.makeFair();
        actor.start();
        return actor;
    }

    /**
     * Creates an instance of StaticDispatchActor.
     *
     * @param code The closure specifying the only statically dispatched message handler.
     * @return The new started actor
     */
    public final Actor staticMessageHandler(@DelegatesTo(Actor.class) final Closure code) {
        final StaticDispatchActor<Object> actor = new StaticDispatchActor<Object>() {
            @Override
            public void onMessage(final Object message) {
                code.call(message);
            }
        };
        code.setDelegate(actor);
        code.setResolveStrategy(Closure.DELEGATE_FIRST);
        actor.setParallelGroup(this);
        actor.start();
        return actor;
    }

    /**
     * Creates an instance of StaticDispatchActor, which will cooperate in thread sharing with other actors sharing the same thread pool.
     *
     * @param code The closure specifying the only statically dispatched message handler.
     * @return The new started actor
     */
    public final Actor fairStaticMessageHandler(@DelegatesTo(Actor.class) final Closure code) {
        final StaticDispatchActor<Object> actor = new StaticDispatchActor<Object>() {
            @Override
            public void onMessage(final Object message) {
                code.call(message);
            }
        };
        code.setDelegate(actor);
        code.setResolveStrategy(Closure.DELEGATE_FIRST);
        actor.setParallelGroup(this);
        actor.makeFair();
        actor.start();
        return actor;
    }

    /**
     * Creates an agent instance initialized with the given state
     *
     * @param state The initial internal state of the new Agent instance
     * @return The created instance
     */
    public final <T> Agent<T> agent(final T state) {
        final Agent<T> safe = new Agent<T>(state);
        safe.attachToThreadPool(threadPool);
        return safe;
    }

    /**
     * Creates an agent instance initialized with the given state
     *
     * @param state The initial internal state of the new Agent instance
     * @param copy  A closure to use to create a copy of the internal state when sending the internal state out
     * @return The created instance
     */
    public final <T> Agent<T> agent(final T state, final Closure copy) {
        final Agent<T> safe = new Agent<T>(state, copy);
        safe.attachToThreadPool(threadPool);
        return safe;
    }

    /**
     * Creates an agent instance initialized with the given state, which will cooperate in thread sharing with other agents and actors in a fair manner.
     *
     * @param state The initial internal state of the new Agent instance
     * @return The created instance
     */
    public final <T> Agent<T> fairAgent(final T state) {
        final Agent<T> safe = agent(state);
        safe.makeFair();
        return safe;
    }

    /**
     * Creates an agent instance initialized with the given state, which will cooperate in thread sharing with other agents and actors in a fair manner.
     *
     * @param copy  A closure to use to create a copy of the internal state when sending the internal state out
     * @param state The initial internal state of the new Agent instance
     * @return The created instance
     */
    public final <T> Agent<T> fairAgent(final T state, final Closure copy) {
        final Agent<T> safe = agent(state, copy);
        safe.makeFair();
        return safe;
    }

    /**
     * Creates a new task assigned to a thread from the current parallel group.
     * Tasks are a lightweight version of dataflow operators, which do not define their communication channels explicitly,
     * but can only exchange data using explicit DataflowVariables and Streams.
     * Registers itself with Dataflow for nested 'whenBound' handlers to use the same group.
     *
     * @param code The task body to run
     * @return A DataflowVariable, which gets assigned the value returned from the supplied code
     */
    public final <T> Promise<T> task(final Closure<T> code) {
        final Closure<T> clonedCode = (Closure<T>) code.clone();
        return task(new Callable<T>() {
            @Override
            public T call() throws Exception {
                return clonedCode.call();
            }
        });
    }

    /**
     * Creates a new task assigned to a thread from the current parallel group.
     * Tasks are a lightweight version of dataflow operators, which do not define their communication channels explicitly,
     * but can only exchange data using explicit DataflowVariables and Streams.
     * Registers itself with Dataflow for nested 'whenBound' handlers to use the same group.
     *
     * @param callable The task body to run
     * @return A DataflowVariable, which gets assigned the value returned from the supplied code
     */
    public final <T> Promise<T> task(final Callable<T> callable) {
        final DataflowVariable result = new DataflowVariable();
        threadPool.execute(new Runnable() {
            @SuppressWarnings("OverlyBroadCatchBlock")
            @Override
            public void run() {
                Dataflow.activeParallelGroup.set(PGroup.this);
                try {
                    //noinspection OverlyBroadCatchBlock
                    try {
                        result.bind(callable.call());
                    } catch (Throwable e) {
                        result.bind(e);
                    }
                } finally {
                    Dataflow.activeParallelGroup.remove();
                }
            }
        });
        return result;
    }

    /**
     * Creates a new task assigned to a thread from the current parallel group.
     * Tasks are a lightweight version of dataflow operators, which do not define their communication channels explicitly,
     * but can only exchange data using explicit DataflowVariables and Streams.
     * Registers itself with Dataflow for nested 'whenBound' handlers to use the same group.
     *
     * @param code The task body to run
     * @return A DataflowVariable, which gets bound to null once the supplied code finishes
     */
    public final Promise<Object> task(final Runnable code) {
        if (code instanceof Closure) return task((Closure) code);
        final DataflowVariable result = new DataflowVariable();
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                Dataflow.activeParallelGroup.set(PGroup.this);
                try {
                    try {
                        code.run();
                        result.bind(null);
                    } catch (Throwable e) {
                        result.bind(e);
                    }
                } finally {
                    Dataflow.activeParallelGroup.remove();
                }
            }
        });
        return result;
    }

    /**
     * Creates a new task assigned to a thread from the current parallel group.
     * The task is lazy, since it only gets executed if the returned Promise instance is read or a then-callback is registered on it.
     * Tasks are a lightweight version of dataflow operators, which do not define their communication channels explicitly,
     * but can only exchange data using explicit DataflowVariables and Streams.
     * Registers itself with Dataflow for nested 'whenBound' handlers to use the same group.
     *
     * @param code The task body to run
     * @return A LazyDataflowVariable, which gets assigned the value returned from the supplied code
     */
    public final <T> Promise<T> lazyTask(final Closure<T> code) {
        final Closure<T> clonedCode = (Closure<T>) code.clone();
        return new LazyDataflowVariable<T>(this, clonedCode);
    }

    /**
     * Creates a new task assigned to a thread from the current parallel group.
     * The task is lazy, since it only gets executed if the returned Promise instance is read or a then-callback is registered on it.
     * Tasks are a lightweight version of dataflow operators, which do not define their communication channels explicitly,
     * but can only exchange data using explicit DataflowVariables and Streams.
     * Registers itself with Dataflow for nested 'whenBound' handlers to use the same group.
     *
     * @param callable The task body to run
     * @return A LazyDataflowVariable, which gets assigned the value returned from the supplied code
     */
    public final <T> Promise<T> lazyTask(final Callable<T> callable) {
        if (callable instanceof Closure) return lazyTask((Closure<T>) callable);
        return new LazyDataflowVariable<T>(this, new Closure<T>(this) {
            @Override public int getMaximumNumberOfParameters() {
                return 0;
            }

            @Override public T call() {
                try {
                    return callable.call();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    /**
     * Creates an operator using the current parallel group
     *
     * @param channels A map specifying "inputs" and "outputs" - dataflow channels (instances of the DataflowQueue or DataflowVariable classes) to use for inputs and outputs
     * @param code     The operator's body to run each time all inputs have a value to read
     * @return A new started operator instance with all the channels set
     */
    public final DataflowProcessor operator(final Map channels, @DelegatesTo(DataflowOperator.class) final Closure code) {
        return new DataflowOperator(this, channels, code).start();
    }

    /**
     * Creates an operator using the current parallel group
     *
     * @param inputChannels  dataflow channels to use for input
     * @param outputChannels dataflow channels to use for output
     * @param code           The operator's body to run each time all inputs have a value to read
     * @return A new started operator instance with all the channels set
     */
    public final DataflowProcessor operator(final List inputChannels, final List outputChannels, @DelegatesTo(DataflowOperator.class) final Closure code) {
        final HashMap<String, List> params = new HashMap<String, List>(5);
        params.put(DataflowProcessor.INPUTS, inputChannels);
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        return new DataflowOperator(this, params, code).start();
    }

    /**
     * Creates an operator using the current parallel group
     *
     * @param inputChannels  dataflow channels to use for input
     * @param outputChannels dataflow channels to use for output
     * @param maxForks       Number of parallel threads running operator's body, defaults to 1
     * @param code           The operator's body to run each time all inputs have a value to read
     * @return A new started operator instance with all the channels set
     */
    public final DataflowProcessor operator(final List inputChannels, final List outputChannels, final int maxForks, @DelegatesTo(DataflowOperator.class) final Closure code) {
        final HashMap<String, Object> params = new HashMap<String, Object>(5);
        params.put(DataflowProcessor.INPUTS, inputChannels);
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        params.put(DataflowProcessor.MAX_FORKS, maxForks);
        return new DataflowOperator(this, params, code).start();
    }

    /**
     * Creates an operator using the current parallel group
     *
     * @param input  a dataflow channel to use for input
     * @param output a dataflow channel to use for output
     * @param code   The operator's body to run each time all inputs have a value to read
     * @return A new started operator instance with all the channels set
     */
    public final DataflowProcessor operator(final DataflowReadChannel input, final DataflowWriteChannel output, @DelegatesTo(DataflowOperator.class) final Closure code) {
        final HashMap<String, List> params = new HashMap<String, List>(5);
        params.put(DataflowProcessor.INPUTS, asList(input));
        params.put(DataflowProcessor.OUTPUTS, asList(output));
        return new DataflowOperator(this, params, code).start();
    }

    /**
     * Creates an operator using the current parallel group
     *
     * @param input    a dataflow channel to use for input
     * @param output   a dataflow channel to use for output
     * @param maxForks Number of parallel threads running operator's body, defaults to 1
     * @param code     The operator's body to run each time all inputs have a value to read
     * @return A new started operator instance with all the channels set
     */
    public final DataflowProcessor operator(final DataflowReadChannel input, final DataflowWriteChannel output, final int maxForks, @DelegatesTo(DataflowOperator.class) final Closure code) {
        final HashMap<String, Object> params = new HashMap<String, Object>(5);
        params.put(DataflowProcessor.INPUTS, asList(input));
        params.put(DataflowProcessor.OUTPUTS, asList(output));
        params.put(DataflowProcessor.MAX_FORKS, maxForks);
        return new DataflowOperator(this, params, code).start();
    }

    /**
     * Creates a selector using this parallel group
     *
     * @param channels A map specifying "inputs" and "outputs" - dataflow channels (instances of the DataflowQueue or DataflowVariable classes) to use for inputs and outputs
     * @param code     The selector's body to run each time a value is available in any of the inputs channels
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor selector(final Map channels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        return new DataflowSelector(this, channels, code).start();
    }

    /**
     * Creates a selector using this parallel group
     *
     * @param inputChannels  dataflow channels to use for input
     * @param outputChannels dataflow channels to use for output
     * @param code           The selector's body to run each time a value is available in any of the inputs channels
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor selector(final List inputChannels, final List outputChannels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        final HashMap<String, List> params = new HashMap<String, List>(5);
        params.put(DataflowProcessor.INPUTS, inputChannels);
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        return new DataflowSelector(this, params, code).start();
    }

    /**
     * Creates a selector using this parallel group. Since no body is provided, the selector will simply copy the incoming values to all output channels.
     *
     * @param channels A map specifying "inputs" and "outputs" - dataflow channels (instances of the DataflowQueue or DataflowVariable classes) to use for inputs and outputs
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor selector(final Map channels) {
        return new DataflowSelector(this, channels, new DataflowProcessorAtomicBoundAllClosure()).start();
    }

    /**
     * Creates a selector using this parallel group. Since no body is provided, the selector will simply copy the incoming values to all output channels.
     *
     * @param inputChannels  dataflow channels to use for input
     * @param outputChannels dataflow channels to use for output
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor selector(final List inputChannels, final List outputChannels) {
        final HashMap<String, List> params = new HashMap<String, List>(5);
        params.put(DataflowProcessor.INPUTS, inputChannels);
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        return new DataflowSelector(this, params, new DataflowProcessorAtomicBoundAllClosure()).start();
    }

    /**
     * Creates a prioritizing selector using the default dataflow parallel group
     * Input with lower position index have higher priority.
     *
     * @param channels A map specifying "inputs" and "outputs" - dataflow channels (instances of the DataflowQueue or DataflowVariable classes) to use for inputs and outputs
     * @param code     The selector's body to run each time a value is available in any of the inputs channels
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor prioritySelector(final Map channels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        return new DataflowPrioritySelector(this, channels, code).start();
    }

    /**
     * Creates a prioritizing selector using the default dataflow parallel group
     * Input with lower position index have higher priority.
     *
     * @param inputChannels  dataflow channels to use for input
     * @param outputChannels dataflow channels to use for output
     * @param code           The selector's body to run each time a value is available in any of the inputs channels
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor prioritySelector(final List inputChannels, final List outputChannels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        final HashMap<String, Object> params = new HashMap<String, Object>(5);
        params.put(DataflowProcessor.INPUTS, inputChannels);
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        return new DataflowPrioritySelector(this, params, code).start();
    }

    /**
     * Creates a prioritizing selector using the default dataflow parallel group. Since no body is provided, the selector will simply copy the incoming values to all output channels.
     * Input with lower position index have higher priority.
     *
     * @param channels A map specifying "inputs" and "outputs" - dataflow channels (instances of the DataflowQueue or DataflowVariable classes) to use for inputs and outputs
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor prioritySelector(final Map channels) {
        return new DataflowPrioritySelector(this, channels, new DataflowProcessorAtomicBoundAllClosure()).start();
    }

    /**
     * Creates a prioritizing selector using the default dataflow parallel group. Since no body is provided, the selector will simply copy the incoming values to all output channels.
     * Input with lower position index have higher priority.
     *
     * @param inputChannels  dataflow channels to use for input
     * @param outputChannels dataflow channels to use for output
     * @return A new started selector instance with all the channels set
     */
    public final DataflowProcessor prioritySelector(final List inputChannels, final List outputChannels) {
        final HashMap<String, Object> params = new HashMap<String, Object>(5);
        params.put(DataflowProcessor.INPUTS, inputChannels);
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        return new DataflowPrioritySelector(this, params, new DataflowProcessorAtomicBoundAllClosure()).start();
    }

    /**
     * Creates a splitter copying its single input channel into all of its output channels. The created splitter will be part of this parallel group
     *
     * @param inputChannel   The channel to  read values from
     * @param outputChannels A list of channels to output to
     * @return A new started splitter instance with all the channels set
     */
    public final DataflowProcessor splitter(final DataflowReadChannel inputChannel, final List<DataflowWriteChannel> outputChannels) {
        if (inputChannel == null || outputChannels == null || outputChannels.isEmpty())
            throw new IllegalArgumentException(A_SPLITTER_NEEDS_AN_INPUT_CHANNEL_AND_AT_LEAST_ONE_OUTPUT_CHANNEL_TO_BE_CREATED);
        final HashMap<String, List> params = new HashMap<String, List>(5);
        params.put(DataflowProcessor.INPUTS, asList(inputChannel));
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        return new DataflowOperator(this, params, new DataflowProcessorAtomicBoundAllClosure()).start();
    }

    /**
     * Creates a splitter copying its single input channel into all of its output channels. The created splitter will be part of this parallel group
     *
     * @param inputChannel   The channel to  read values from
     * @param outputChannels A list of channels to output to
     * @param maxForks       Number of threads running the splitter's body, defaults to 1
     * @return A new started splitter instance with all the channels set
     */
    public final DataflowProcessor splitter(final DataflowReadChannel inputChannel, final List<DataflowWriteChannel> outputChannels, final int maxForks) {
        if (inputChannel == null || outputChannels == null || outputChannels.isEmpty())
            throw new IllegalArgumentException(A_SPLITTER_NEEDS_AN_INPUT_CHANNEL_AND_AT_LEAST_ONE_OUTPUT_CHANNEL_TO_BE_CREATED);
        final HashMap<String, Object> params = new HashMap<String, Object>(5);
        params.put(DataflowProcessor.INPUTS, asList(inputChannel));
        params.put(DataflowProcessor.OUTPUTS, outputChannels);
        params.put(DataflowProcessor.MAX_FORKS, maxForks);
        return new DataflowOperator(this, params, new DataflowProcessorAtomicBoundAllClosure()).start();
    }

    /**
     * Creates a select using the current parallel group. The returns Select instance will allow the user to
     * obtain values from the supplied dataflow variables or streams as they become available.
     *
     * @param channels Dataflow variables or streams to wait for values on
     */
    public final Select select(final SelectableChannel... channels) {
        return new Select(this, channels);
    }

    /**
     * Creates a select using the current parallel group. The returns Select instance will allow the user to
     * obtain values from the supplied dataflow variables or streams as they become available.
     *
     * @param channels Dataflow variables or streams to wait for values on
     */
    public final Select select(final List<SelectableChannel> channels) {
        return new Select(this, channels);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promises The promises to wait for
     * @param code     A closure to execute with concrete values for each of the supplied promises
     * @param <T>      The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final List<Promise> promises, final Closure<T> code) {
        return whenAllBound(promises, code, null);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1 The promises to wait for
     * @param code     A closure to execute with concrete values for each of the supplied promises
     * @param <T>      The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Closure<T> code) {
        return whenAllBound(asList(promise1), code, null);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1 The promises to wait for
     * @param promise2 The promises to wait for
     * @param code     A closure to execute with concrete values for each of the supplied promises
     * @param <T>      The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Closure<T> code) {
        return whenAllBound(asList(promise1, promise2), code, null);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1 The promises to wait for
     * @param promise2 The promises to wait for
     * @param promise3 The promises to wait for
     * @param code     A closure to execute with concrete values for each of the supplied promises
     * @param <T>      The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Closure<T> code) {
        return whenAllBound(asList(promise1, promise2, promise3), code, null);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1 The promises to wait for
     * @param promise2 The promises to wait for
     * @param promise3 The promises to wait for
     * @param promise4 The promises to wait for
     * @param code     A closure to execute with concrete values for each of the supplied promises
     * @param <T>      The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Promise promise4, final Closure<T> code) {
        return whenAllBound(asList(promise1, promise2, promise3, promise4), code, null);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promises     The promises to wait for
     * @param code         A closure to execute with concrete values for each of the supplied promises
     * @param errorHandler A closure handling an exception (an instance of Throwable), if it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final List<Promise> promises, final Closure<T> code, final Closure<T> errorHandler) {
        if (promises.size() != code.getMaximumNumberOfParameters() && !isListAccepting(code)) {
            throw new IllegalArgumentException("Cannot run whenAllBound(), since the number of promises does not match the number of arguments to the supplied closure.");
        }
        for (final Promise promise : promises) {
            promise.touch();
        }
        final DataflowVariable result = new DataflowVariable();
        whenAllBound(promises, 0, new ArrayList<Object>(promises.size()), result, code, errorHandler);
        return result;
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1     The promises to wait for
     * @param code         A closure to execute with concrete values for each of the supplied promises
     * @param errorHandler A closure handling an exception (an instance of Throwable), if if it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Closure<T> code, final Closure<T> errorHandler) {
        return whenAllBound(asList(promise1), code, errorHandler);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1     The promises to wait for
     * @param promise2     The promises to wait for
     * @param code         A closure to execute with concrete values for each of the supplied promises
     * @param errorHandler A closure handling an exception (an instance of Throwable), if if it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Closure<T> code, final Closure<T> errorHandler) {
        return whenAllBound(asList(promise1, promise2), code, errorHandler);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1     The promises to wait for
     * @param promise2     The promises to wait for
     * @param promise3     The promises to wait for
     * @param code         A closure to execute with concrete values for each of the supplied promises
     * @param errorHandler A closure handling an exception (an instance of Throwable), if if it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Closure<T> code, final Closure<T> errorHandler) {
        return whenAllBound(asList(promise1, promise2, promise3), code, errorHandler);
    }

    /**
     * Without blocking the thread waits for all the promises to get bound and then passes them to the supplied closure.
     *
     * @param promise1     The promises to wait for
     * @param promise2     The promises to wait for
     * @param promise3     The promises to wait for
     * @param promise4     The promises to wait for
     * @param code         A closure to execute with concrete values for each of the supplied promises
     * @param errorHandler A closure handling an exception (an instance of Throwable), if if it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public final <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Promise promise4, final Closure<T> code, final Closure<T> errorHandler) {
        return whenAllBound(asList(promise1, promise2, promise3, promise4), code, errorHandler);
    }

    /**
     * Waits for the promise identified by the index to be bound and then passes on to the next promise in the list
     *
     * @param promises A list of all promises that need to be waited for
     * @param index    The index of the current promise to wait for
     * @param values   A list of values the so-far processed promises were bound tpo
     * @param result   The promise for the final result of the calculation
     * @param code     The calculation to execute on the values once they are all bound
     * @param <T>      The type of the final result
     */
    private <T> void whenAllBound(final List<Promise> promises, final int index, final List<Object> values, final DataflowVariable<T> result, final Closure<T> code, final Closure<T> errorHandler) {
        if (index == promises.size()) {
            try {
                if (isListAccepting(code)) {
                    result.leftShift(code.call(values));
                } else {
                    result.leftShift(code.call(values.toArray()));
                }
            } catch (Throwable e) {
                result.bindError(e);
            }
        } else promises.get(index).then(this, new MessagingRunnable<Object>() {
                    @Override
                    protected void doRun(final Object argument) {
                        values.add(argument);
                        whenAllBound(promises, index + 1, values, result, code, errorHandler);
                    }
                }, new MessagingRunnable() {
                    @Override
                    protected void doRun(final Object argument) {
                        if (errorHandler != null && shallHandle(errorHandler, (Throwable) argument)) {
                            try {
                                result.leftShift(errorHandler.getMaximumNumberOfParameters() == 1 ? errorHandler.call(argument) : errorHandler.call());
                            } catch (Throwable e) {
                                result.bindError(e);
                            }
                        } else {
                            result.bindError((Throwable) argument);
                        }

                    }
                }
        );
    }

    private <T> boolean shallHandle(final Closure<T> errorHandler, final Throwable e) {
        final Class[] types = errorHandler.getParameterTypes();
        if (types.length == 0) return true;
        return types[0].isAssignableFrom(e.getClass());
    }

    private static <T> boolean isListAccepting(final Closure<T> code) {
        return code.getMaximumNumberOfParameters() == 1 && List.class.isAssignableFrom(code.getParameterTypes()[0]);
    }

    /**
     * Shutdown the thread pool gracefully
     */
    @SuppressWarnings({"FinalizeDeclaration"})
    @Override
    protected void finalize() throws Throwable {
        this.threadPool.shutdown();
        super.finalize();
    }

    /**
     * Resizes the thread pool to the specified value
     *
     * @param poolSize The new pool size
     */
    public void resize(final int poolSize) {
        threadPool.resize(poolSize);
    }

    /**
     * Sets the pool size to the default
     */
    public void resetDefaultSize() {
        threadPool.resetDefaultSize();
    }

    /**
     * Retrieves the current thread pool size
     *
     * @return The pool size
     */
    public int getPoolSize() {
        return threadPool.getPoolSize();
    }

    /**
     * schedules a new task for processing with the pool
     *
     * @param task The task to schedule
     */
    public void execute(final Runnable task) {
        threadPool.execute(task);
    }

    /**
     * Gently stops the pool
     */
    public void shutdown() {
        threadPool.shutdown();
    }
}
