/*
 * 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.InputPort;
import cc.redberry.pipe.OutputPort;
import cc.redberry.pipe.OutputPortCloseable;
import cc.redberry.pipe.util.ExceptionHandler;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Merges data streams form several output ports.
 *
 * <p>It starts a new thread for each attached output port to pull data from it and place it into output buffer.</p>
 *
 * @param <E> type of merged stream objects
 * @author Bolotin Dmitriy
 * @author Mikhail Shugay
 */
public final class Merger<E> implements OutputPortCloseable<E> {
    final Buffer<E> buffer;
    final List<OutputPort<? extends E>> inputPorts = new ArrayList<>();
    final List<Future<?>> futures = new ArrayList<>();
    final List<Thread> threads = new ArrayList<>();
    final List<O2ITransmitter> transmitters = new ArrayList<>();
    private final ExceptionHandler<O2ITransmitter> exceptionsHandler = new ExceptionHandler<O2ITransmitter>() {
        @Override
        public void handle(RuntimeException exception, O2ITransmitter source) {
            if (thrown == null)
                thrown = exception;
            close();
        }
    };
    private final AtomicBoolean closed = new AtomicBoolean(false);
    List<ExecutorService> targetExecutors = new ArrayList<>();
    private boolean failSafe = false;
    private volatile RuntimeException thrown = null;

    /**
     * Creates merger with default output buffer size.
     */
    public Merger() {
        this(Buffer.DEFAULT_SIZE);
    }

    /**
     * Creates merger with defined output buffer size.
     *
     * @param bufferSize size of output buffer
     */
    public Merger(int bufferSize) {
        this.buffer = new Buffer<>(bufferSize);
    }

    public void setFailSafe(boolean failSafe) {
        this.failSafe = failSafe;
    }

    /**
     * Attache output port to merger.
     *
     * <p>Each port will be processed in individual thread, so no concurrent calls to {@link
     * cc.redberry.pipe.OutputPort#take()} will be performed.</p>
     *
     * @param port            port to be served
     * @param executorService executor service that should be used to create new thread to serve provided output port
     */
    public void merge(OutputPort<E> port, ExecutorService executorService) {
        if (targetExecutors == null)
            throw new RuntimeException("This merger is already started.");
        inputPorts.add(port);
        targetExecutors.add(executorService);
    }

    /**
     * Attache output port to merger.
     *
     * <p>Each port will be processed in individual thread, so no concurrent calls to {@link
     * cc.redberry.pipe.OutputPort#take()} will be performed.</p>
     *
     * @param port port to be served
     */
    public void merge(OutputPort<E> port) {
        if (targetExecutors == null)
            throw new RuntimeException("This merger is already started.");
        inputPorts.add(port);
        targetExecutors.add(null);
    }

    /**
     * Starts threads for current merger
     */
    public synchronized void start() {
        if (targetExecutors == null)
            throw new RuntimeException("This merger is already started.");

        ExecutorService service;
        OutputPort<? extends E> port;
        O2ITransmitter<E> task;

        int i;

        InputPort<E>[] iPorts = new InputPort[inputPorts.size()];
        for (i = 0; i < inputPorts.size(); ++i)
            iPorts[i] = buffer.createInputPort();

        for (i = 0; i < inputPorts.size(); ++i) {
            service = targetExecutors.get(i);
            port = inputPorts.get(i);
            transmitters.add(task = new O2ITransmitter<>(port, iPorts[i], exceptionsHandler));
            if (service == null) {
                Thread thread = new Thread(task);
                threads.add(thread);
                thread.start();
            } else
                futures.add(service.submit(task));
        }
        targetExecutors = null;
    }

    /**
     * Join all threads owned by this buffer.
     *
     * @throws RuntimeException     if some of {@link cc.redberry.pipe.OutputPort#take()} thrown som exception
     * @throws InterruptedException
     */
    public void join() throws InterruptedException {
        try {
            for (Future<?> future : futures)
                future.get();
        } catch (ExecutionException ee) {
            throw new RuntimeException("This expression should not be thrown.", ee);
        }
        for (Thread thread : threads)
            thread.join();
        if (Thread.interrupted())
            throw new InterruptedException();
    }

    /**
     * Take object from output buffer
     *
     * @return {@inheritDoc}
     * @throws InterruptedException
     */
    @Override
    public E take() {
        E result = buffer.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 e = thrown;
                        thrown = null;
                        throw e;
                    }
                }
            return null;
        }

        return result;
    }

    /**
     * Stops current merger.
     *
     * <p>This method propagates close signal to upstream (input) output port.</p>
     */
    @Override
    public void close() {
        if (!closed.compareAndSet(false, true))
            return;

        for (OutputPort<? extends E> input : inputPorts)
            if (input instanceof OutputPortCloseable)
                ((OutputPortCloseable) input).close();

        for (O2ITransmitter<E> tasks : transmitters)
            tasks.stop();

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

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

}
