// 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.dataflow;

import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import groovyx.gpars.GParsConfig;
import groovyx.gpars.dataflow.operator.DataflowOperator;
import groovyx.gpars.dataflow.operator.DataflowProcessor;
import groovyx.gpars.dataflow.operator.DataflowSelector;
import groovyx.gpars.group.DefaultPGroup;
import groovyx.gpars.group.PGroup;

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

import static java.util.Arrays.asList;

/**
 * Contains factory methods to create dataflow actors and starting them.
 *
 * @author Vaclav Pech, Dierk Koenig
 *         Date: Jun 4, 2009
 */
@SuppressWarnings({"rawtypes", "RawUseOfParameterizedType", "AbstractClassWithoutAbstractMethods", "AbstractClassNeverImplemented", "ConstantDeclaredInAbstractClass", "UtilityClass", "unchecked"})
public abstract class Dataflow {

    /**
     * The parallel group used by all Dataflow Concurrency actors by default.
     */
    public static final PGroup DATA_FLOW_GROUP = new DefaultPGroup(GParsConfig.retrieveDefaultPool());

    /**
     * Maps threads/tasks to parallel groups they belong to
     */
    public static final ThreadLocal<PGroup> activeParallelGroup = new ThreadLocal<PGroup>();

    /**
     * Retrieves the thread-local value of the active PGroup or the default DataflowGroup
     *
     * @return The PGroup to use for DF within the current thread
     */
    public static PGroup retrieveCurrentDFPGroup() {
        PGroup pGroup = activeParallelGroup.get();
        if (pGroup == null) {
            pGroup = Dataflow.DATA_FLOW_GROUP;
        }
        return pGroup;
    }

    /**
     * Sets the supplied PGroup as the default for the given block of code. All dataflow functions, such as task or operator
     * and callback handlers, will use the PGroup and its thread pool for their scheduling.
     *
     * @param group The group to make the default inside the block
     * @param code  The code to run with overriden default
     * @return The value returned from the supplied code block
     */
    public static Object usingGroup(final PGroup group, final Closure code) {
        final PGroup original = activeParallelGroup.get();
        try {
            activeParallelGroup.set(group);
            return code.call();
        } finally {
            activeParallelGroup.set(original);
        }
    }

    /**
     * Creates a new task assigned to a thread from the default dataflow 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.
     *
     * @param code The task body to run
     * @return A DataflowVariable, which gets assigned the value returned from the supplied code
     */
    public static <T> Promise<T> task(final Closure<T> code) {
        return retrieveCurrentDFPGroup().task(code);
    }

    /**
     * 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 static <T> Promise<T> task(final Callable<T> callable) {
        return retrieveCurrentDFPGroup().task(callable);
    }

    /**
     * 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 runnable The task body to run
     * @return A DataflowVariable, which gets bound to null once the supplied code finishes
     */
    public static Promise<Object> task(final Runnable runnable) {
        return retrieveCurrentDFPGroup().task(runnable);
    }

    /**
     * Creates a new task assigned to a thread from the default dataflow 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.
     *
     * @param code The task body to run
     * @return A LazyDataflowVariable, which gets assigned the value returned from the supplied code
     */
    public static <T> Promise<T> lazyTask(final Closure<T> code) {
        final PGroup group = retrieveCurrentDFPGroup();
        return group.lazyTask(code);
    }

    /**
     * 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 static <T> Promise<T> lazyTask(final Callable<T> callable) {
        return retrieveCurrentDFPGroup().lazyTask(callable);
    }

    /**
     * Creates an operator using the default dataflow 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 active operator instance
     */
    public static DataflowProcessor operator(final Map channels, @DelegatesTo(DataflowOperator.class) final Closure code) {
        return retrieveCurrentDFPGroup().operator(channels, code);
    }

    /**
     * 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 active operator instance
     */
    public static DataflowProcessor operator(final List inputChannels, final List outputChannels, @DelegatesTo(DataflowOperator.class) final Closure code) {
        return retrieveCurrentDFPGroup().operator(inputChannels, outputChannels, code);
    }

    /**
     * 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 active operator instance
     */
    public static DataflowProcessor operator(final List inputChannels, final List outputChannels, final int maxForks, @DelegatesTo(DataflowOperator.class) final Closure code) {
        return retrieveCurrentDFPGroup().operator(inputChannels, outputChannels, maxForks, code);
    }

    /**
     * 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 active operator instance
     */
    public static DataflowProcessor operator(final DataflowReadChannel input, final DataflowWriteChannel output, @DelegatesTo(DataflowOperator.class) final Closure code) {
        return retrieveCurrentDFPGroup().operator(input, output, code);
    }

    /**
     * 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 active operator instance
     */
    public static DataflowProcessor operator(final DataflowReadChannel input, final DataflowWriteChannel output, final int maxForks, @DelegatesTo(DataflowOperator.class) final Closure code) {
        return retrieveCurrentDFPGroup().operator(input, output, maxForks, code);
    }

    /**
     * Creates a selector using the default dataflow 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 active selector instance
     */
    public static DataflowProcessor selector(final Map channels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        return retrieveCurrentDFPGroup().selector(channels, code);
    }

    /**
     * Creates a selector using the default dataflow 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 active selector instance
     */
    public static DataflowProcessor selector(final List inputChannels, final List outputChannels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        return retrieveCurrentDFPGroup().selector(inputChannels, outputChannels, code);
    }

    /**
     * Creates a selector using the default dataflow 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 active selector instance
     */
    public static DataflowProcessor selector(final Map channels) {
        return retrieveCurrentDFPGroup().selector(channels);
    }

    /**
     * Creates a selector using the default dataflow 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 active selector instance
     */
    public static DataflowProcessor selector(final List inputChannels, final List outputChannels) {
        return retrieveCurrentDFPGroup().selector(inputChannels, outputChannels);
    }

    /**
     * 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 active selector instance
     */
    public static DataflowProcessor prioritySelector(final Map channels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        return retrieveCurrentDFPGroup().prioritySelector(channels, code);
    }

    /**
     * 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 active selector instance
     */
    public static DataflowProcessor prioritySelector(final List inputChannels, final List outputChannels, @DelegatesTo(DataflowSelector.class) final Closure code) {
        return retrieveCurrentDFPGroup().prioritySelector(inputChannels, outputChannels, code);
    }

    /**
     * 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 active selector instance
     */
    public static DataflowProcessor prioritySelector(final Map channels) {
        return retrieveCurrentDFPGroup().prioritySelector(channels);
    }

    /**
     * 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 active selector instance
     */
    public static DataflowProcessor prioritySelector(final List inputChannels, final List outputChannels) {
        return retrieveCurrentDFPGroup().prioritySelector(inputChannels, outputChannels);
    }

    /**
     * Creates a splitter copying its single input channel into all of its output channels. The created splitter will be part of the default parallel group
     * Input with lower position index have higher priority.
     *
     * @param inputChannel   The channel to  read values from
     * @param outputChannels A list of channels to output to
     * @return A new active splitter instance
     */
    public static DataflowProcessor splitter(final DataflowReadChannel inputChannel, final List<DataflowWriteChannel> outputChannels) {
        return retrieveCurrentDFPGroup().splitter(inputChannel, outputChannels);
    }

    /**
     * Creates a splitter copying its single input channel into all of its output channels. The created splitter will be part of this parallel group
     * Input with lower position index have higher priority.
     *
     * @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 active splitter instance
     */
    public static DataflowProcessor splitter(final DataflowReadChannel inputChannel, final List<DataflowWriteChannel> outputChannels, final int maxForks) {
        return retrieveCurrentDFPGroup().splitter(inputChannel, outputChannels, maxForks);
    }

    /**
     * Creates a select using the default dataflow 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
     * @return A new select instance
     */
    public static Select<?> select(final SelectableChannel<?>... channels) {
        return retrieveCurrentDFPGroup().select(channels);
    }

    /**
     * Creates a select using the default dataflow 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
     * @return A new select instance
     */
    public static Select<?> select(final List<SelectableChannel> channels) {
        return retrieveCurrentDFPGroup().select(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 static <T> Promise<T> whenAllBound(final List<Promise> promises, final Closure<T> code) {
        return retrieveCurrentDFPGroup().whenAllBound(promises, code);
    }

    /**
     * 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 static <T> Promise<T> whenAllBound(final Promise promise1, final Closure<T> code) {
        return retrieveCurrentDFPGroup().whenAllBound(asList(promise1), code);
    }

    /**
     * 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 static <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Closure<T> code) {
        return retrieveCurrentDFPGroup().whenAllBound(asList(promise1, promise2), code);
    }

    /**
     * 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 static <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Closure<T> code) {
        return retrieveCurrentDFPGroup().whenAllBound(asList(promise1, promise2, promise3), code);
    }

    /**
     * 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 static <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Promise promise4, final Closure<T> code) {
        return retrieveCurrentDFPGroup().whenAllBound(asList(promise1, promise2, promise3, promise4), code);
    }

    /**
     * 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 static <T> Promise<T> whenAllBound(final List<Promise> promises, final Closure<T> code, final Closure<T> errorHandler) {
        return retrieveCurrentDFPGroup().whenAllBound(promises, 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 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 static <T> Promise<T> whenAllBound(final Promise promise1, final Closure<T> code, final Closure<T> errorHandler) {
        return retrieveCurrentDFPGroup().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 it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public static <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Closure<T> code, final Closure<T> errorHandler) {
        return retrieveCurrentDFPGroup().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 it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public static <T> Promise<T> whenAllBound(final Promise promise1, final Promise promise2, final Promise promise3, final Closure<T> code, final Closure<T> errorHandler) {
        return retrieveCurrentDFPGroup().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 it gets bound
     * @param <T>          The type of the final result
     * @return A promise for the final result
     */
    public static <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 retrieveCurrentDFPGroup().whenAllBound(asList(promise1, promise2, promise3, promise4), code, errorHandler);
    }
}
