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

import groovy.lang.Closure;
import groovyx.gpars.actor.impl.MessageStream;
import groovyx.gpars.dataflow.DataCallback;
import groovyx.gpars.dataflow.Dataflow;
import groovyx.gpars.dataflow.DataflowChannelListener;
import groovyx.gpars.dataflow.DataflowReadChannel;
import groovyx.gpars.dataflow.DataflowVariable;
import groovyx.gpars.dataflow.expression.DataflowExpression;

import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Represents a common base for publish-subscribe deterministic channels.
 *
 * @param <T> Type for values to pass through the channels
 * @author Johannes Link, Vaclav Pech
 */
public abstract class StreamCore<T> implements FList<T> {

    protected final DataflowVariable<T> first;
    protected final AtomicReference<StreamCore<T>> rest = new AtomicReference<StreamCore<T>>();

    /**
     * A collection of listeners who need to be informed each time the stream is bound to a value
     */
    protected final Collection<MessageStream> wheneverBoundListeners;

    /**
     * Creates an empty stream
     *
     * @param first The variable to store as the head of the stream
     */
    protected StreamCore(final DataflowVariable<T> first) {
        this.first = first;
        wheneverBoundListeners = new CopyOnWriteArrayList<MessageStream>();
    }

    /**
     * Creates a stream while applying the supplied initialization closure to it
     *
     * @param first       The variable to store as the head of the stream
     * @param toBeApplied The closure to use for initialization
     */
    protected StreamCore(final DataflowVariable<T> first, final Closure toBeApplied) {
        this(first);
        apply(toBeApplied);
    }

    @SuppressWarnings({"AssignmentToCollectionOrArrayFieldFromParameter"})
    protected StreamCore(final DataflowVariable<T> first, final Collection<MessageStream> wheneverBoundListeners, final Collection<DataflowChannelListener<T>> updateListeners) {
        this.first = first;
        this.wheneverBoundListeners = wheneverBoundListeners;
        hookWheneverBoundListeners(first);
        addUpdateListeners(updateListeners);
    }

    private void addUpdateListeners(final Collection<DataflowChannelListener<T>> updateListeners) {
        first.getEventManager().addAllDataflowChannelListeners(updateListeners);
    }

    final void addUpdateListener(final DataflowChannelListener<T> updateListener) {
        first.getEventManager().addDataflowChannelListener(updateListener);
    }

    public static <T> T eos() {
        return null;
    }

    private static <T> T eval(final Object valueOrDataflowVariable) {
        if (valueOrDataflowVariable instanceof DataflowVariable)
            try {
                return ((DataflowReadChannel<T>) valueOrDataflowVariable).getVal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        return (T) valueOrDataflowVariable;
    }

    /**
     * Populates the stream with generated values
     *
     * @param seed      The initial element to evaluate and add as the first value of the stream
     * @param generator A closure generating stream elements from the previous values
     * @param condition A closure indicating whether the generation should continue based on the last generated value
     * @return This stream
     */
    public final StreamCore<T> generate(final T seed, final Closure generator, final Closure condition) {
        generateNext(seed, this, generator, condition);
        return this;
    }

    private void generateNext(final T value, final StreamCore<T> stream, final Closure generator, final Closure condition) {
        T recurValue = value;
        StreamCore<T> recurStream = stream;
        while (true) {
            final boolean addValue = (Boolean) condition.call(new Object[]{recurValue});
            if (!addValue) {
                recurStream.leftShift(StreamCore.<T>eos());
                return;
            }
            recurStream = recurStream.leftShift(recurValue);
            recurValue = (T) eval(generator.call(new Object[]{recurValue}));
        }
    }

    /**
     * Calls the supplied closure with the stream as a parameter
     *
     * @param closure The closure to call
     * @return This instance of DataflowStream
     */
    public final StreamCore<T> apply(final Closure closure) {
        closure.call(new Object[]{this});
        return this;
    }

    /**
     * Adds a dataflow variable value to the stream, once the value is available
     *
     * @param ref The DataflowVariable to check for value
     * @return The rest of the stream
     */
    public final StreamCore<T> leftShift(final DataflowReadChannel<T> ref) {
        ref.getValAsync(new MessageStream() {
            @Override
            public MessageStream send(final Object message) {
                first.bind((T) message);
                return null;
            }
        });
        return (StreamCore<T>) getRest();
    }

    /**
     * Adds a value to the stream
     *
     * @param value The value to add
     * @return The rest of the stream
     */
    public final StreamCore<T> leftShift(final T value) {
        bind(value);
        return (StreamCore<T>) getRest();
    }

    private void bind(final T value) {
        first.bind(value);
    }

    final DataflowVariable<T> getFirstDFV() {
        return first;
    }

    /**
     * Retrieved the first element in the stream, blocking until a value is available
     *
     * @return The first item in the stream
     */
    @Override
    public final T getFirst() {
        try {
            return first.getVal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieves a DataflowStream representing the rest of this Stream after removing the first element
     *
     * @return The remaining stream elements
     */
    @Override
    public abstract FList<T> getRest();

    /**
     * Indicates, whether the first element in the stream is an eos
     */
    @Override
    public final boolean isEmpty() {
        return getFirst() == eos();
    }

    /**
     * Builds a filtered stream using the supplied filter closure
     *
     * @param filterClosure The closure to decide on inclusion of elements
     * @return The first item of the filtered stream
     */
    @Override
    public final FList<T> filter(final Closure filterClosure) {
        final StreamCore<T> newStream = createNewStream();
        filter(this, filterClosure, newStream);
        return newStream;
    }

    private void filter(final StreamCore<T> rest, final Closure filterClosure, final StreamCore<T> result) {
        StreamCore<T> recurRest = rest;
        StreamCore<T> recurResult = result;
        while (true) {
            if (recurRest.isEmpty()) {
                recurResult.leftShift(StreamCore.<T>eos());
                return;
            }
            final boolean include = (Boolean) eval(filterClosure.call(new Object[]{recurRest.getFirst()}));
            if (include) recurResult = recurResult.leftShift(recurRest.getFirst());
            recurRest = (StreamCore<T>) recurRest.getRest();
        }
    }

    /**
     * Builds a modified stream using the supplied map closure
     *
     * @param mapClosure The closure to transform elements
     * @return The first item of the transformed stream
     */
    @Override
    public final FList<Object> map(final Closure mapClosure) {
        final StreamCore<Object> newStream = (StreamCore<Object>) createNewStream();
        map(this, mapClosure, newStream);
        return newStream;
    }

    private void map(final FList<T> rest, final Closure mapClosure, final StreamCore<Object> result) {
        FList<T> recurRest = rest;
        StreamCore<Object> recurResult = result;
        while (true) {
            if (recurRest.isEmpty()) {
                recurResult.leftShift(StreamCore.eos());
                return;
            }
            final Object mapped = mapClosure.call(new Object[]{recurRest.getFirst()});
            recurResult = recurResult.leftShift(eval(mapped));
            recurRest = recurRest.getRest();
        }
    }

    /**
     * Reduces all elements in the stream using the supplied closure
     *
     * @param reduceClosure The closure to reduce elements of the stream gradually into an accumulator. The accumulator is seeded with the first stream element.
     * @return The result of reduction of the whole stream
     */
    @Override
    public final T reduce(final Closure reduceClosure) {
        if (isEmpty())
            return null;
        return reduce(getFirst(), getRest(), reduceClosure);
    }

    /**
     * Reduces all elements in the stream using the supplied closure
     *
     * @param reduceClosure The closure to reduce elements of the stream gradually into an accumulator.
     * @param seed          The value to initialize the accumulator with.
     * @return The result of reduction of the whole stream
     */
    @Override
    public final T reduce(final T seed, final Closure reduceClosure) {
        return reduce(seed, this, reduceClosure);
    }

    private T reduce(final T current, final FList<T> rest, final Closure reduceClosure) {
        T recurCurrent = current;
        FList<T> recurRest = rest;
        while (true) {
            if (recurRest.isEmpty())
                return recurCurrent;
            final Object aggregate = reduceClosure.call(new Object[]{recurCurrent, recurRest.getFirst()});
            recurCurrent = (T) eval(aggregate);
            recurRest = recurRest.getRest();
        }
    }

    /**
     * Builds an iterator to iterate over the stream
     *
     * @return A new FListIterator instance
     */
    @Override
    public final Iterator<T> iterator() {
        return new FListIterator<T>(this);
    }

    @Override
    public String appendingString() {
        if (!first.isBound())
            return ", ?";
        if (isEmpty())
            return "";
        return ", " + getFirst() + getRest().appendingString();
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        final FList stream = (FList) obj;
        if (isEmpty())
            return stream.isEmpty();
        if (!getFirst().equals(stream.getFirst()))
            return false;
        return getRest().equals(stream.getRest());
    }

    @Override
    public int hashCode() {
        int result = first.hashCode();
        result = 31 * result + rest.hashCode();
        return result;
    }

    /**
     * A factory method to create new instances of the correct class when needed
     *
     * @return An instance of the appropriate sub-class
     */
    protected abstract StreamCore<T> createNewStream();

    public final void wheneverBound(final Closure closure) {
        wheneverBoundListeners.add(new DataCallback(closure, Dataflow.retrieveCurrentDFPGroup()));
        first.whenBound(closure);
    }

    public final void wheneverBound(final MessageStream stream) {
        wheneverBoundListeners.add(stream);
        first.whenBound(stream);
    }

    /**
     * Hooks the registered when bound handlers to the supplied dataflow expression
     *
     * @param expr The expression to hook all the when bound listeners to
     * @return The supplied expression handler to allow method chaining
     */
    private DataflowExpression<T> hookWheneverBoundListeners(final DataflowExpression<T> expr) {
        for (final MessageStream listener : wheneverBoundListeners) {
            expr.whenBound(listener);
        }
        return expr;
    }

    /**
     * Increases the number of parties required to perform the data exchange
     */
    public void incrementParties() {
    }

    /**
     * Decreases the number of parties required to perform the data exchange
     */
    public void decrementParties() {
    }
}
