//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// This file is a part of the 'coroutines' project.
// Copyright 2018 Elmar Sonnenschein, esoco GmbH, Flensburg, Germany
//
// 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 de.esoco.coroutine.step;

import de.esoco.coroutine.Continuation;
import de.esoco.coroutine.Coroutine;
import de.esoco.coroutine.CoroutineStep;
import de.esoco.coroutine.Selection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;


/********************************************************************
 * A coroutine step that suspends the coroutine execution until the results of
 * several other asynchronously executed coroutines are available. The results
 * are then collected and handed over to the resuming step. By default the
 * results of all finished coroutines will be collected but that can be modified
 * by setting a different condition with {@link #when(Predicate)}. Also by
 * default all coroutines will be awaited before resuming but that can be
 * controlled with {@link #until(Predicate)}. If the collecting is finished all
 * coroutines that are still running will be cancelled.
 *
 * <p>To select exactly only one result from multiple coroutines the related
 * step implementation {@link Select} can be used.</p>
 *
 * @author eso
 */
public class Collect<I, O> extends CoroutineStep<I, Collection<O>>
{
	//~ Instance fields --------------------------------------------------------

	private List<Coroutine<? super I, ? extends O>> aCoroutines =
		new ArrayList<>();

	private Predicate<Continuation<?>> pCollectCritiera    = c -> true;
	private Predicate<Continuation<?>> pCompletionCritiera = c -> false;

	//~ Constructors -----------------------------------------------------------

	/***************************************
	 * Creates a new instance.
	 *
	 * @param rFromCoroutines The coroutines to select from
	 */
	public Collect(
		Collection<Coroutine<? super I, ? extends O>> rFromCoroutines)
	{
		if (rFromCoroutines.size() == 0)
		{
			throw new IllegalArgumentException(
				"At least one coroutine to collect is required");
		}

		aCoroutines.addAll(rFromCoroutines);
	}

	/***************************************
	 * Copies the state of another instance.
	 *
	 * @param rOther The other instance
	 */
	private Collect(Collect<I, O> rOther)
	{
		aCoroutines.addAll(rOther.aCoroutines);

		pCollectCritiera    = rOther.pCollectCritiera;
		pCompletionCritiera = rOther.pCompletionCritiera;
	}

	//~ Static methods ---------------------------------------------------------

	/***************************************
	 * Suspends the coroutine execution until all coroutines finish and then
	 * resumes the execution with a collection of the results. By default the
	 * result of the first finished coroutine is collected. That can be modified
	 * by providing a different selection condition to {@link #until(Predicate)}
	 * which will return a new {@link Collect} instance. Modified instances that
	 * select from additional coroutines or steps can be created with {@link
	 * #and(Coroutine)} and {@link #and(CoroutineStep)}.
	 *
	 * @param  rFromCoroutines The coroutines to select from
	 *
	 * @return A new step instance
	 */
	@SafeVarargs
	public static <I, O> Collect<I, O> collect(
		Coroutine<? super I, ? extends O>... rFromCoroutines)
	{
		return new Collect<I, O>(asList(rFromCoroutines));
	}

	/***************************************
	 * Suspends the coroutine execution until one coroutine step finishes. The
	 * step arguments will be wrapped into new coroutines and then handed to
	 * {@link #collect(Coroutine...)}.
	 *
	 * @param  rFromSteps The coroutine steps to select from
	 *
	 * @return A new step instance
	 */
	@SafeVarargs
	public static <I, O> Collect<I, O> collect(
		CoroutineStep<? super I, ? extends O>... rFromSteps)
	{
		return new Collect<>(
			asList(rFromSteps).stream()
			.map(rStep -> new Coroutine<>(rStep))
			.collect(Collectors.toList()));
	}

	//~ Methods ----------------------------------------------------------------

	/***************************************
	 * Creates a new instance that collects the result of an additional
	 * coroutine.
	 *
	 * @param  rCoroutine The additional coroutine to select from
	 *
	 * @return The new instance
	 */
	public Collect<I, O> and(Coroutine<? super I, ? extends O> rCoroutine)
	{
		Collect<I, O> aCollect = new Collect<>(this);

		aCollect.aCoroutines.add(rCoroutine);

		return aCollect;
	}

	/***************************************
	 * Creates a new instance that collects the result of an additional step.
	 * The step will be wrapped into a new coroutine and handed to {@link
	 * #and(Coroutine)}.
	 *
	 * @param  rStep The additional step to select from
	 *
	 * @return The new instance
	 */
	public Collect<I, O> and(CoroutineStep<? super I, ? extends O> rStep)
	{
		Collect<I, O> aCollect = new Collect<>(this);

		aCollect.aCoroutines.add(new Coroutine<>(rStep));

		return aCollect;
	}

	/***************************************
	 * {@inheritDoc}
	 */
	@Override
	public void runAsync(CompletableFuture<I>			 fPreviousExecution,
						 CoroutineStep<Collection<O>, ?> rNextStep,
						 Continuation<?>				 rContinuation)
	{
		fPreviousExecution.thenAcceptAsync(
			rInput -> collectAsync(rInput, rNextStep, rContinuation));
	}

	/***************************************
	 * Adds a condition for the termination of the result collection. If a
	 * succefully finished continuation matches the given predicate the
	 * collection will be completed and the suspension resumed with the result
	 * that has been collected so far.
	 *
	 * <p>Any collection criteria provided through {@link #when(Predicate)} are
	 * not automatically applied to the completion criteria and must therefore
	 * be handled explicitly in the completion test if necessary.</p>
	 *
	 * @param  pCompletionCriteria A condition that checks if a result should be
	 *                             selected
	 *
	 * @return A new step instance
	 */
	public Collect<I, O> until(Predicate<Continuation<?>> pCompletionCriteria)
	{
		Collect<I, O> aCollect = new Collect<>(aCoroutines);

		aCollect.pCompletionCritiera = pCompletionCriteria;

		return aCollect;
	}

	/***************************************
	 * Adds a condition for the result collection. If a succefully finished
	 * continuation matches the given predicate it will be collected into the
	 * step result.
	 *
	 * @param  pCollectCriteria A condition that checks if a result should be
	 *                          collected
	 *
	 * @return A new step instance
	 */
	public Collect<I, O> when(Predicate<Continuation<?>> pCollectCriteria)
	{
		Collect<I, O> aCollect = new Collect<>(aCoroutines);

		aCollect.pCollectCritiera = pCollectCriteria;

		return aCollect;
	}

	/***************************************
	 * {@inheritDoc}
	 */
	@Override
	protected Collection<O> execute(I rInput, Continuation<?> rContinuation)
	{
		// even if executed blocking the selection must happen asynchronously,
		// so we just run this step as a new coroutine in the current scope
		return new Coroutine<>(this).runAsync(rContinuation.scope(), rInput)
									.getResult();
	}

	/***************************************
	 * Performs the asynchronous collection.
	 *
	 * @param rInput        The input value
	 * @param rNextStep     The step to resume after the suspension
	 * @param rContinuation the current continuation
	 */
	void collectAsync(I								  rInput,
					  CoroutineStep<Collection<O>, ?> rNextStep,
					  Continuation<?>				  rContinuation)
	{
		Selection<Collection<O>, O, Collection<O>> aSelection =
			Selection.ofMultipleValues(
				this,
				rNextStep,
				rContinuation,
				pCompletionCritiera,
				pCollectCritiera);

		rContinuation.suspendTo(aSelection);

		aCoroutines.forEach(
			rCoroutine ->
		{
			aSelection.add(rCoroutine.runAsync(rContinuation.scope(), rInput));
		});

		aSelection.seal();
	}
}