/*
 * 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;


import cc.redberry.pipe.blocks.Merger;
import cc.redberry.pipe.util.Chunk;
import cc.redberry.pipe.util.SimpleProcessorWrapper;
import cc.redberry.pipe.util.SimpleProcessorWrapperSynchronized;
import cc.redberry.pipe.util.SimpleVoidProcessorExecutor;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Utility methods.
 */
public final class CUtils {
    public static final OutputPort EMPTY_OUTPUT_PORT = new OutputPort() {
        @Override
        public Object take() {
            return null;
        }
    };
    public static final OutputPortCloseable EMPTY_OUTPUT_PORT_CLOSEABLE = new OutputPortCloseable() {
        @Override
        public void close() {
        }

        @Override
        public Object take() {
            return null;
        }
    };

    /**
     * Wraps processor and input port into single output port.
     *
     * @param inputPort output port to be wrapped
     * @param processor processor to wrap
     * @param <InputT>  type of input objects
     * @param <OutputT> type of output objects
     * @return wrapped output port
     */
    public static <InputT, OutputT> OutputPort<OutputT> wrap(OutputPort<? extends InputT> inputPort, Processor<? super InputT, ? extends OutputT> processor) {
        return new SimpleProcessorWrapper<>(inputPort, processor);
    }

    /**
     * Wraps processor and input port into single output port. Wrapper will introduce additional synchronization.
     *
     * @param inputPort output port to be wrapped
     * @param processor processor to wrap
     * @param <InputT>  type of input objects
     * @param <OutputT> type of output objects
     * @return wrapped output port
     */
    public static <InputT, OutputT> OutputPort<OutputT> wrapSynchronized(OutputPort<? extends InputT> inputPort, Processor<? super InputT, ? extends OutputT> processor) {
        return new SimpleProcessorWrapperSynchronized<>(inputPort, processor);
    }

    /**
     * Returns void processor wrapper of an input port.
     *
     * @param inputPort input port to wrap
     * @param <T>       type of objects
     * @return wrapped input port
     */
    public static <T> VoidProcessor<T> asVoidProcessor(final InputPort<T> inputPort) {
        return new VoidProcessor<T>() {
            @Override
            public void process(T input) {
                inputPort.put(input);
            }
        };
    }

    /**
     * Drains all content of output port into input port.<br/> This method will close a to port by putting null into
     * it.
     *
     * @param from port to take elements from
     * @param to   port to put elements to
     * @param <T>  type of elements
     * @throws InterruptedException
     */
    public static <T> void drain(OutputPort<? extends T> from, InputPort<? super T> to) throws InterruptedException {
        T element;
        while ((element = from.take()) != null)
            to.put(element);
        to.put(null); //Closing
    }

    /**
     * Drains all content of output port into input port.
     *
     * @param from port to take elements from
     * @param to   port to put elements to
     * @param <T>  type of elements
     * @throws InterruptedException
     */
    public static <T> void drainWithoutClose(OutputPort<T> from, InputPort<T> to) throws InterruptedException {
        T element;
        while ((element = from.take()) != null)
            to.put(element);
    }

    /**
     * Converts VoidProcessor to Processor
     *
     * @param processor VoidProcessor to convert
     * @return Processor wrapper of VoidProcessor
     */
    public static <T> Processor<T, T> asProcessor(final VoidProcessor<T> processor) {
        return new Processor<T, T>() {
            @Override
            public T process(T input) {
                processor.process(input);
                return input;
            }
        };
    }

    /**
     * Process no more than {@code maxCount} elements from input by provided processor in current thread.
     *
     * @param input     input object stream
     * @param processor void processor
     * @param maxCount  maximum number of objects to process
     * @param <T>       type of processed objects
     * @return true if some objects left in input, false if not
     * @throws InterruptedException
     */
    public static <T> boolean processPart(OutputPort<T> input, VoidProcessor<T> processor, int maxCount) throws InterruptedException {
        if (maxCount <= 0)
            throw new IllegalArgumentException("maxCount should be greater than 0");
        T object = null;
        while ((--maxCount) >= 0 && (object = input.take()) != null)
            processor.process(object);
        return object != null;
    }

    /**
     * Process all elements from output port in current thread.
     *
     * @param input     input object stream
     * @param processor void processor
     * @param <T>       type of processed objects
     * @throws InterruptedException
     */
    public static <T> void processAll(OutputPort<T> input, VoidProcessor<? super T> processor) throws InterruptedException {
        T object;
        while ((object = input.take()) != null)
            processor.process(object);
    }

    /**
     * Process all elements from output port in current thread.
     *
     * @param input       input object stream
     * @param processor   void processor
     * @param threadCount number of parallel threads
     * @param <T>         type of processed objects
     * @throws InterruptedException
     */
    public static <T> void processAllInParallel(OutputPort<T> input, VoidProcessor<? super T> processor, int threadCount)
            throws InterruptedException {
        --threadCount;
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; ++i) {
            threads[i] = new Thread(new SimpleVoidProcessorExecutor<>(input, processor));
            threads[i].start();
        }

        T object;
        while ((object = input.take()) != null)
            processor.process(object);

        for (int i = 0; i < threadCount; ++i)
            threads[i].join();
    }

    /**
     * Process all elements from output port in current thread.
     *
     * @param input           input object stream
     * @param processor       void processor
     * @param threadCount     number of parallel threads
     * @param executorService executor service to create threads
     * @param <T>             type of processed objects
     * @throws InterruptedException
     */
    public static <T> void processAllInParallel(OutputPort<T> input, VoidProcessor<? super T> processor, int threadCount,
                                                ExecutorService executorService) throws InterruptedException {
        --threadCount;
        Future<?>[] futures = new Future[threadCount];
        for (int i = 0; i < threadCount; ++i)
            futures[i] = executorService.submit(new SimpleVoidProcessorExecutor<>(input, processor));
        T object;
        while ((object = input.take()) != null)
            processor.process(object);
        try {
            for (int i = 0; i < threadCount; ++i)
                futures[i].get();
        } catch (ExecutionException ex) {
            throw (RuntimeException) ex.getCause();
        }
    }

    /**
     * Process all elements from output port in current thread.
     *
     * @param input             input object stream
     * @param processorsFactory void processor factory
     * @param threadCount       number of parallel threads
     * @param <T>               type of processed objects
     * @throws InterruptedException
     */
    public static <T> void processAllInParallel(OutputPort<T> input, VoidProcessorFactory<? super T> processorsFactory, int threadCount)
            throws InterruptedException {
        --threadCount;
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; ++i) {
            threads[i] = new Thread(new SimpleVoidProcessorExecutor<T>(input, processorsFactory.create()));
            threads[i].start();
        }

        VoidProcessor<? super T> processor = processorsFactory.create();
        T object;
        while ((object = input.take()) != null)
            processor.process(object);

        for (int i = 0; i < threadCount; ++i)
            threads[i].join();
    }

    /**
     * Processes elements in N different threads. For each thread a separate processor is created by a provided
     * factory.<br/> In this implementation threads are created by an ExecutorsService.
     *
     * @param input             elements to process
     * @param processorsFactory factory to createInputStream processors
     * @param threadCount       count of parallel processing threads
     * @param executorService   service to execute this task (thread pool)
     * @param <T>               type of processed objects
     */
    public static <T> void processAllInParallel(OutputPort<T> input, VoidProcessorFactory<? super T> processorsFactory, int threadCount,
                                                ExecutorService executorService) throws InterruptedException {
        --threadCount;
        Future<?>[] futures = new Future[threadCount];
        for (int i = 0; i < threadCount; ++i)
            futures[i] = executorService.submit(new SimpleVoidProcessorExecutor<>(input, processorsFactory.create()));

        VoidProcessor<? super T> processor = processorsFactory.create();
        T object;
        while ((object = input.take()) != null)
            processor.process(object);
        try {
            for (int i = 0; i < threadCount; ++i)
                futures[i].get();
        } catch (ExecutionException ex) {
            throw (RuntimeException) ex.getCause();
        }
    }

    public static <T> OutputPort<T> asOutputPort(Iterable<T> iterable) {
        final Iterator<T> iterator = iterable.iterator();
        return new OutputPort<T>() {
            @Override
            public synchronized T take() {
                if (iterator.hasNext())
                    return iterator.next();
                else
                    return null;
            }
        };
    }

    public static <T> OutputPort<T> asUnsafeOutputPort(Iterable<T> iterable) {
        final Iterator<T> iterator = iterable.iterator();
        return new OutputPort<T>() {
            @Override
            public T take() {
                if (iterator.hasNext())
                    return iterator.next();
                else
                    return null;
            }
        };
    }

    public static <T> OutputPort<Chunk<T>> chunked(final OutputPortCloseable<T> input, final int chunkSize) {
        return new OutputPortCloseable<Chunk<T>>() {
            @Override
            public void close() {
                input.close();
            }

            @Override
            public Chunk<T> take() {
                return Chunk.readChunk(input, chunkSize);
            }
        };
    }

    public static <T> OutputPort<Chunk<T>> chunked(final OutputPort<T> input, final int chunkSize) {
        if (input instanceof OutputPortCloseable)
            return chunked((OutputPortCloseable<T>) input, chunkSize);
        else
            return new OutputPort<Chunk<T>>() {
                @Override
                public Chunk<T> take() {
                    return Chunk.readChunk(input, chunkSize);
                }
            };
    }

    public static <T> OutputPortCloseable<T> unchunked(OutputPort<Chunk<T>> chunkedPort) {
        return new UnChunkingPort<>(chunkedPort);
    }

    public static <I, O> Processor<Chunk<I>, Chunk<O>> chunked(final Processor<I, O> processor) {
        return new Processor<Chunk<I>, Chunk<O>>() {
            @Override
            public Chunk<O> process(Chunk<I> input) {
                Object[] buffer = new Object[input.size()];
                for (int i = 0; i < input.size(); ++i)
                    buffer[i] = processor.process(input.get(i));
                return new Chunk<>(buffer);
            }
        };
    }

    public static <I> VoidProcessor<Chunk<I>> chunked(final VoidProcessor<I> processor) {
        return new VoidProcessor<Chunk<I>>() {
            @Override
            public void process(Chunk<I> input) {
                for (I i : input)
                    processor.process(i);
            }
        };
    }

    public static <T> Merger<T> buffered(OutputPort<T> input, int bufferSize) {
        Merger<T> merger = new Merger<>(bufferSize);
        merger.merge(input);
        merger.start();
        return merger;
    }

    public static <T> Merger<T> buffered(OutputPort<T> input, int bufferSize, ExecutorService executorService) {
        Merger<T> merger = new Merger<>(bufferSize);
        merger.merge(input, executorService);
        merger.start();
        return merger;
    }

    public static <T> OutputPort<T> asOutputPort(T... array) {
        return asOutputPort(Arrays.asList(array));
    }

    public static <T> OutputPort<T> asOutputPort(final List<T> list) {
        return new OutputPort<T>() {
            final AtomicInteger counter = new AtomicInteger();

            @Override
            public T take() {
                int index = counter.getAndIncrement();
                if (index >= list.size())
                    return null;
                return list.get(index);
            }
        };
    }

    public static <T> Iterable<T> it(final OutputPort<T> input) {
        return new Iterable<T>() {
            @Override
            public Iterator<T> iterator() {
                return new OPIterator<>(input);
            }
        };
    }

    public static class OPIterator<T> implements Iterator<T> {
        protected final OutputPort<T> op;
        T next = null;

        public OPIterator(OutputPort<T> op) {
            this.op = op;
        }

        @Override
        public boolean hasNext() {
            if (next != null)
                return true;
            else
                return (next = op.take()) != null;
        }

        @Override
        public T next() {
            T n = next;
            next = null;
            return n;
        }

        @Override
        public void remove() {
            throw new IllegalStateException("Not supported.");
        }
    }

    private static final class UnChunkingPort<T> implements OutputPortCloseable<T> {
        final OutputPort<Chunk<T>> innerPort;
        final AtomicReference<ChunkHolder<T>> currentChunk = new AtomicReference();

        private UnChunkingPort(OutputPort<Chunk<T>> innerPort) {
            this.innerPort = innerPort;
        }

        @Override
        public void close() {
            if (innerPort instanceof OutputPortCloseable)
                ((OutputPortCloseable) innerPort).close();
        }

        @Override
        public T take() {
            ChunkHolder<T> holder = currentChunk.get();
            T element;
            while (holder == null || (element = holder.take()) == null) {
                // Current holder is empty

                // Checking if this holder is final in the sequence
                if (holder != null && holder.isFinal())
                    // End of sequence
                    return null;

                // Creating new holder
                ChunkHolder<T> newHolder = new ChunkHolder<>();

                // Trying to acquire reference to holder
                if (currentChunk.compareAndSet(holder, newHolder)) {
                    // Reference to holder successfully acquired
                    // Initializing holder (this call also supports null as input)
                    newHolder.set(innerPort.take());
                    holder = newHolder;
                } else
                    // Somebody already acquired reference to holder
                    // Getting this reference
                    holder = currentChunk.get();
            }
            return element;
        }
    }

    private static final class ChunkHolder<T> implements OutputPort<T> {
        private volatile OutputPort<T> chunkPort;

        private OutputPort<T> getPort() {
            // Spin-loop
            while (chunkPort == null) ;
            return chunkPort;
        }

        @Override
        public T take() {
            return getPort().take();
        }

        public boolean isFinal() {
            return getPort() == EMPTY_OUTPUT_PORT;
        }

        public void set(Chunk<T> value) {
            if (value == null)
                chunkPort = EMPTY_OUTPUT_PORT;
            else
                chunkPort = value.outputPort();
        }
    }
}
