/*
 * cc.redberry.pipe: java library for implementation of concurrent pipelines
 * Copyright (c) 2010-2012
 * Bolotin Dmitriy <bolotin.dmitriy@gmail.com>
 *
 * 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 3 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.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package cc.redberry.pipe.blocks;

import cc.redberry.pipe.*;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * This class helps to process data from one OutputPort in N parallel threads, collecting results in one output buffer.
 *
 * <p>It could be used either to access one processor concurrently or N processors from their corresponding threads. See
 * constructor descriptions for details.</p>
 *
 * @param <InputT>  input type
 * @param <OutputT> output type
 * @author Bolotin Dmitriy
 * @author Mikhail Shugay
 */
public final class ParallelProcessor<InputT, OutputT> implements OutputPortCloseable<OutputT> {
    private final Buffer<OutputT> outputBuffer;
    private final Worker[] workers;
    private final Thread[] threads;
    private final Future<?>[] futures;
    private final OutputPort<InputT> input;
    private volatile boolean closed = false, failSafe = false;
    private volatile RuntimeException thrown = null;

    /**
     * Creates ParallelProcessor working in one thread per processor mode with default buffer size.
     *
     * @param input           Worker to take elements from.
     * @param factory         Factory to createInputStream processors.
     * @param threads         Number of parallel threads for processing. Also it is the number of processors to
     *                        createInputStream.
     * @param executorService Executor service to create threads with.
     */
    public ParallelProcessor(OutputPort<InputT> input, ProcessorFactory<InputT, OutputT> factory, int threads,
                             ExecutorService executorService) {
        this(input, CCUtils.createArray(factory, threads), Buffer.DEFAULT_SIZE, executorService);
    }


    /**
     * Creates ParallelProcessor working in one thread per processor mode.
     *
     * @param input           Worker to take elements from.
     * @param factory         Factory to createInputStream processors.
     * @param bufferSize      Buffer size.
     * @param threads         Number of parallel threads for processing. Also it is the number of processors to
     *                        createInputStream.
     * @param executorService Executor service to create threads with.
     */
    public ParallelProcessor(OutputPort<InputT> input, ProcessorFactory<InputT, OutputT> factory, int bufferSize, int threads,
                             ExecutorService executorService) {
        this(input, CCUtils.createArray(factory, threads), bufferSize, executorService);
    }

    /**
     * Creates ParallelProcessor working in one thread per processor mode.
     *
     * @param input      Worker to take elements from.
     * @param factory    Factory to createInputStream processors.
     * @param bufferSize Buffer size.
     * @param threads    Number of parallel threads of processing. Also it is the number of processors to
     *                   createInputStream.
     */
    public ParallelProcessor(OutputPort<InputT> input, ProcessorFactory<InputT, OutputT> factory, int bufferSize, int threads) {
        this(input, CCUtils.createArray(factory, threads), bufferSize);
    }

    /**
     * Creates ParallelProcessor working in one thread per processor mode with default buffer size.
     *
     * @param input   Worker to take elements from.
     * @param factory Factory to createInputStream processors.
     * @param threads Number of parallel threads of processing. Also it is the number of processors to
     *                createInputStream.
     */
    public ParallelProcessor(OutputPort<InputT> input, ProcessorFactory<InputT, OutputT> factory, int threads) {
        this(input, CCUtils.createArray(factory, threads), Buffer.DEFAULT_SIZE);
    }


    /**
     * Creates ParallelProcessor working in concurrent processor access mode.
     *
     * @param input      Worker to take elements from.
     * @param processor  Processor. It will be accessed concurrently.
     * @param bufferSize Buffer size.
     * @param threads    Number of parallel threads of processing.
     */
    public ParallelProcessor(OutputPort<InputT> input, Processor<InputT, OutputT> processor, int bufferSize, int threads) {
        this(input, toArray(processor, threads), bufferSize);
    }

    /**
     * Creates ParallelProcessor working in concurrent processor access mode with default buffer size.
     *
     * @param input     Worker to take elements from.
     * @param processor Processor. It will be accessed concurrently.
     * @param threads   Number of parallel threads of processing.
     */
    public ParallelProcessor(OutputPort<InputT> input, Processor<InputT, OutputT> processor, int threads) {
        this(input, toArray(processor, threads), Buffer.DEFAULT_SIZE);
    }


    /**
     * Creates ParallelProcessor working in concurrent processor access mode.
     *
     * @param input           Worker to take elements from.
     * @param processor       Processor. It will be accessed concurrently.
     * @param bufferSize      Buffer size.
     * @param threads         Number of parallel threads of processing.
     * @param executorService Executor service to create threads with.
     */
    public ParallelProcessor(OutputPort<InputT> input, Processor<InputT, OutputT> processor, int bufferSize, int threads,
                             ExecutorService executorService) {
        this(input, toArray(processor, threads), bufferSize, executorService);
    }


    /**
     * Creates ParallelProcessor working in one thread per processor mode.
     *
     * @param input      Worker to take elements from.
     * @param processors Processors. Each processor will be accessed only by one thread.
     * @param bufferSize Buffer size.
     */
    public ParallelProcessor(OutputPort<InputT> input, Processor<InputT, OutputT>[] processors, int bufferSize) {
        this.input = input;
        this.outputBuffer = new Buffer<>(bufferSize);
        this.workers = new ParallelProcessor.Worker[processors.length];
        this.threads = new Thread[processors.length];
        this.futures = null;

        int i;
        InputPort<OutputT>[] iPorts = new InputPort[processors.length];
        for (i = 0; i < processors.length; ++i)
            iPorts[i] = this.outputBuffer.createInputPort();


        for (i = 0; i < processors.length; ++i) {
            this.workers[i] = new Worker(iPorts[i], processors[i]);
            this.threads[i] = new Thread(this.workers[i]);
            this.threads[i].start();
        }
    }

    /**
     * Creates ParallelProcessor working in one thread per processor mode.
     *
     * @param input           Worker to take elements from.
     * @param processors      Processors. Each processor will be accessed only by one thread.
     * @param bufferSize      Buffer size.
     * @param executorService Executor service to create threads with.
     */
    public ParallelProcessor(OutputPort<InputT> input, Processor<InputT, OutputT>[] processors, int bufferSize,
                             ExecutorService executorService) {
        this.input = input;
        this.outputBuffer = new Buffer<>(bufferSize);
        this.workers = new ParallelProcessor.Worker[processors.length];
        this.threads = null;
        this.futures = new Future[processors.length];


        int i;
        InputPort<OutputT>[] iPorts = new InputPort[processors.length];
        for (i = 0; i < processors.length; ++i)
            iPorts[i] = this.outputBuffer.createInputPort();

        for (i = 0; i < processors.length; ++i) {
            this.workers[i] = new Worker(iPorts[i], processors[i]);
            this.futures[i] = executorService.submit(this.workers[i]);
        }
    }

    private static <InputT, OutputT> Processor<InputT, OutputT>[] toArray(Processor<InputT, OutputT> processor, int size) {
        //if (size > 1
        //        && !(processor instanceof ThreadSafe))
        //    throw new IllegalArgumentException("Processor not marked as thread-safe. Use ThreadSafe interface to mark " +
        //            "thread-safe Processor implementations.");

        Processor<InputT, OutputT>[] processors = new Processor[size];
        for (int i = 0; i < size; ++i)
            processors[i] = processor;
        return processors;
    }

    public ParallelProcessor<InputT, OutputT> setFailSafe(boolean failSafe) {
        this.failSafe = failSafe;
        return this;
    }

    /**
     * Join all threads owned by this parallel processor.
     *
     * @throws InterruptedException if this thread was interrupted
     */
    public void join() throws InterruptedException {
        if (futures == null)
            for (Thread t : threads)
                t.join();
        else
            try {
                for (Future<?> future : futures)
                    future.get();
            } catch (ExecutionException ee) {
                throw new RuntimeException("This exception should not be thrown.", ee);
            }
        if (thrown != null)
            throw new RuntimeException(thrown);

        if (Thread.interrupted())
            throw new InterruptedException();
    }

    /**
     * Take first object that was processed so far from output buffer.
     *
     * @return {@inheritDoc}
     */
    public OutputT take() {
        OutputT result = outputBuffer.take();

        // Checking whether some processor has thrown an exception if buffer is closed
        if (result == null) {
            if (thrown != null)
                synchronized (this) {
                    if (thrown != null) {
                        RuntimeException re = thrown;
                        thrown = null;
                        throw re;
                    }
                }
            return null;
        }

        return result;
    }

    @Override
    public String toString() {
        return outputBuffer.toString();
    }

    /**
     * Stops current processor.
     *
     * <p>Some elements taken from input may be dropped, and may never reach the output if {@code failSafe} flag is set
     * to false.</p>
     *
     * <p>This method propagates close signal to upstream (input) output port.</p>
     */
    @Override
    public void close() {
        closed = true;

        if (input instanceof OutputPortCloseable)
            ((OutputPortCloseable) input).close();

        if (!failSafe)
            outputBuffer.close();
    }

    private class Worker implements Runnable {
        private final InputPort<OutputT> toBuffer;
        private final Processor<InputT, OutputT> processor;

        private Worker(InputPort<OutputT> toBuffer, Processor<InputT, OutputT> processor) {
            this.toBuffer = toBuffer;
            this.processor = processor;
        }

        @Override
        public void run() {
            //For performance
            final OutputPort<InputT> input = ParallelProcessor.this.input;

            try {
                InputT inputValue;
                OutputT outputValue;
                while (!closed && (inputValue = input.take()) != null) {

                    outputValue = processor.process(inputValue);
                    toBuffer.put(outputValue);

                    if (Thread.interrupted()) //Just in case
                        return;
                }
            } catch (PInterruptedException in) {
            } catch (RuntimeException re) {
                if (thrown == null)
                    synchronized (ParallelProcessor.this) {
                        if (thrown == null)
                            thrown = re;
                    }
                close(); //Close all!
            } finally {
                toBuffer.put(null); //close upstream input port in any case
            }
        }
    }
}
