/**
 * Copyright 2015-2020 Valery Silaev (http://vsilaev.com)
 *
 * 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 net.tascalate.concurrent;

import static net.tascalate.concurrent.SharedFunctions.cancelPromise;
import static net.tascalate.concurrent.SharedFunctions.selectFirst;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Spliterators;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;


/**
 * Utility class to create a resolved (either successfully or faulty) {@link Promise}-s; 
 * to wrap an arbitrary {@link CompletionStage} interface to the {@link Promise} API;
 * to combine several {@link CompletionStage}-s into aggregating promise.
 *    
 * @author vsilaev
 *
 */
public final class Promises {
    
    public static enum Cancel {
        NONE(CompletionIterator.CancelStrategy.NONE),
        ENLISTED(CompletionIterator.CancelStrategy.ENLISTED),
        ALL(CompletionIterator.CancelStrategy.ALL),;
        private final CompletionIterator.CancelStrategy strategy;
        private Cancel(CompletionIterator.CancelStrategy strategy) {
            this.strategy = strategy;
        }
    }

    private Promises() {}
    
    /**
     * Method to create a successfully resolved {@link Promise} with a value provided 
     * @param <T>
     *   a type of the value
     * @param value
     *   a value to wrap
     * @return 
     *   a successfully resolved {@link Promise} with a value provided
     */
    public static <T> Promise<T> success(T value) {
        return new CompletableFutureWrapper<>(
            CompletableFuture.completedFuture(value)
        );
    }
    
    /**
     * Method to create a faulty resolved {@link Promise} with an exception provided 
     * @param <T>
     *   a type of the value 
     * @param exception
     *   an exception to wrap
     * @return 
     *   a faulty resolved {@link Promise} with an exception provided
     */    
    public static <T> Promise<T> failure(Throwable exception) {
        CompletableFuture<T> delegate = new CompletableFuture<>();
        delegate.completeExceptionally(exception);
        return new CompletableFutureWrapper<>(delegate);
    }

    
    public static <T> Promise<T> maybe(Optional<T> maybeValue) {
        return maybeValue.map(Promises::success)
                         .orElseGet(() -> Promises.failure(new NoSuchElementException()));
    }
    
    /**
     * Adapts a stage passed to the {@link Promise} API
     * @param <T>
     *   a type of the value
     * @param stage
     *   a {@link CompletionStage} to be wrapped
     * @return
     *   a {@link Promise}
     */
    public static <T> Promise<T> from(CompletionStage<T> stage) {
        if (stage instanceof Promise) {
            return (Promise<T>) stage;
        }

        if (stage instanceof CompletableFuture) {
            return new CompletableFutureWrapper<>((CompletableFuture<T>)stage);
        }
        
        /*
        return transform(stage, Function.identity(), Function.identity());
         */
        return CompletionStageWrapper.from(stage);
    }
    
    public static <T> CompletionStage<T> withDefaultExecutor(CompletionStage<T> stage, Executor executor) {
        return new ExecutorBoundCompletionStage<>(stage, executor);
    }
    
    public static Throwable unwrapCompletionException(Throwable ex) {
        return SharedFunctions.unwrapCompletionException(ex);
    }
    
    public static <T> Iterator<T> iterateCompletions(Stream<? extends CompletionStage<? extends T>> pendingPromises, 
                                                     int chunkSize) {
        return iterateCompletions(pendingPromises.iterator(), chunkSize);
    }

    public static <T> Iterator<T> iterateCompletions(Iterable<? extends CompletionStage<? extends T>> pendingPromises, 
                                                     int chunkSize) {
        return iterateCompletions(pendingPromises.iterator(), chunkSize);
    }
    
    private static <T> Iterator<T> iterateCompletions(Iterator<? extends CompletionStage<? extends T>> pendingPromises, 
                                                      int chunkSize) {
        return new CompletionIterator<>(pendingPromises, chunkSize);
    }
    
    public static <T> Stream<T> streamCompletions(Stream<? extends CompletionStage<? extends T>> pendingPromises, 
                                                  int chunkSize) {
        return streamCompletions(pendingPromises, chunkSize, Cancel.ENLISTED);
    }
    
    public static <T> Stream<T> streamCompletions(Stream<? extends CompletionStage<? extends T>> pendingPromises, 
                                                  int chunkSize, Cancel cancelOption) {
        return streamCompletions(pendingPromises.iterator(), chunkSize, cancelOption);
    }

    public static <T> Stream<T> streamCompletions(Iterable<? extends CompletionStage<? extends T>> pendingPromises, 
                                                  int chunkSize) {
        return streamCompletions(pendingPromises, chunkSize, Cancel.ENLISTED);
    }
    
    public static <T> Stream<T> streamCompletions(Iterable<? extends CompletionStage<? extends T>> pendingPromises, 
                                                  int chunkSize, Cancel cancelOption) {
        return streamCompletions(pendingPromises.iterator(), chunkSize, cancelOption);
    }
    
    private static <T> Stream<T> streamCompletions(Iterator<? extends CompletionStage<? extends T>> pendingPromises, 
                                                   int chunkSize, Cancel cancelOption) {
        return toCompletionStream(new CompletionIterator<>(pendingPromises, chunkSize, cancelOption.strategy));
    }
    
    private static <T> Stream<T> toCompletionStream(CompletionIterator<T> iterator) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)
                            .onClose(iterator::close); 
    }

    public static <T, A, R> Promise<R> partitioned(Iterable<? extends T> values, 
                                                   int batchSize, 
                                                   Function<? super T, CompletionStage<? extends T>> spawner, 
                                                   Collector<T, A, R> downstream) {
        return partitioned1(values.iterator(), batchSize, spawner, downstream);
    }
    
    public static <T, A, R> Promise<R> partitioned(Iterable<? extends T> values, 
                                                   int batchSize, 
                                                   Function<? super T, CompletionStage<? extends T>> spawner, 
                                                   Collector<T, A, R> downstream,
                                                   Executor downstreamExecutor) {
        return partitioned2(values.iterator(), batchSize, spawner, downstream, downstreamExecutor);
    }
    
    public static <T, A, R> Promise<R> partitioned(Stream<? extends T> values, 
                                                   int batchSize, 
                                                   Function<? super T, CompletionStage<? extends T>> spawner, 
                                                   Collector<T, A, R> downstream) {
        return partitioned1(values.iterator(), batchSize, spawner, downstream);
    }
    
    public static <T, A, R> Promise<R> partitioned(Stream<? extends T> values, 
                                                   int batchSize, 
                                                   Function<? super T, CompletionStage<? extends T>> spawner, 
                                                   Collector<T, A, R> downstream,
                                                   Executor downstreamExecutor) {
        return partitioned2(values.iterator(), batchSize, spawner, downstream, downstreamExecutor);
    }
    
    
    private static <T, A, R> Promise<R> partitioned1(Iterator<? extends T> values, 
                                                    int batchSize, 
                                                    Function<? super T, CompletionStage<? extends T>> spawner, 
                                                    Collector<T, A, R> downstream) {
        return
            parallelStep1(values, 0, batchSize, null, spawner, downstream)
            .dependent()
            .thenApply(downstream.finisher(), true)
            .as(onCloseSource(values))
            .unwrap();
    }
    

    private static <T, A, R> Promise<R> partitioned2(Iterator<? extends T> values, 
                                                    int batchSize, 
                                                    Function<? super T, CompletionStage<? extends T>> spawner, 
                                                    Collector<T, A, R> downstream,
                                                    Executor downstreamExecutor) {
        return 
            parallelStep2(values, 0, batchSize, null, spawner, downstream, downstreamExecutor)
            .dependent()
            .thenApplyAsync(downstream.finisher(), downstreamExecutor, true)
            .as(onCloseSource(values))
            .unwrap();
    }
    
    private static <T> Function<Promise<T>, Promise<T>> onCloseSource(Object source) {
        if (source instanceof AutoCloseable) {
            return p -> p.dependent().whenComplete((r, e) -> {
                try (AutoCloseable o = (AutoCloseable)source) {
                    
                } catch (Exception ex) {
                    SharedFunctions.sneakyThrow(ex);
                }
            }, true);
        } else {
            return Function.identity();
        }
    }
    
    private static <T, A, R> Promise<A> parallelStep1(Iterator<? extends T> values, 
                                                     int step, 
                                                     int batchSize,
                                                     A current,
                                                     Function<? super T, CompletionStage<? extends T>> spawner,                                                        
                                                     Collector<T, A, R> downstream) {
        
        List<T> valuesBatch = drainBatch(values, batchSize);
        if (valuesBatch.isEmpty()) {
            // Over
            return Promises.success(step == 0 ? downstream.supplier().get() : current)
                           .dependent();
        } else {
            List<CompletionStage<? extends T>> promisesBatch = 
                valuesBatch.stream()
                           .map(spawner)
                           .collect(Collectors.toList());
           
            // This tricky thing (from first promise, then combine with all, then compose) 
            // will propagate all decorators to the resulting promise.
            // Promise.all doesn't inherit decorators
            DependentPromise<? extends T> first = Promises.from(promisesBatch.get(0))
                                                          .dependent(); 
            AtomicBoolean isRecursive = new AtomicBoolean(true);
            Thread invokerThread = Thread.currentThread();
            try {
                return first.thenCombine(Promises.all(promisesBatch), SharedFunctions.selectSecond(), PromiseOrigin.ALL)
                            .thenCompose(vals -> {
                                
                    boolean callLater = isRecursive.get() && Thread.currentThread() == invokerThread;
                    if (callLater) {
                        return 
                        first
                        .thenCombine(Timeouts.delay(MINIMAL_DELAY), selectFirst(), PromiseOrigin.PARAM_ONLY)
                        // yep, default async -- the least evil, while we have to jump off from the timeout thread
                        .thenComposeAsync(__ -> 
                            parallelStep1(values, step + 1, batchSize, 
                                         accumulate(vals, step, current, downstream), 
                                         spawner, downstream), 
                            true);
                    } else {
                        return parallelStep1(values, step + 1, batchSize, 
                                            accumulate(vals, step, current, downstream), 
                                            spawner, downstream);
                    }
                }, true);
            } finally {
                isRecursive.set(false);
            }
        }
    }

    private static <T, A, R> Promise<A> parallelStep2(Iterator<? extends T> values, 
                                                     int step, 
                                                     int batchSize,
                                                     A current,
                                                     Function<? super T, CompletionStage<? extends T>> spawner,                                                        
                                                     Collector<T, A, R> downstream,
                                                     Executor downstreamExecutor) {
        
        List<T> valuesBatch = drainBatch(values, batchSize);
        if (valuesBatch.isEmpty()) {
            // Over
            Promise<A> result;
            if (step == 0) {
                result = CompletableTask.supplyAsync(downstream.supplier(), downstreamExecutor);
            } else {
                result = Promises.success(current);
            }
            return result.dependent();
        } else {
            List<CompletionStage<? extends T>> promisesBatch = 
                valuesBatch.stream()
                           .map(spawner)
                           .collect(Collectors.toList());
           
            // This tricky thing (from first promise, then combine with all, then compose) 
            // will propagate all decorators to the resulting promise.
            // Promise.all doesn't inherit decorators
            return 
                Promises.from(promisesBatch.get(0))
                       .dependent()
                       .thenCombine(Promises.all(promisesBatch), SharedFunctions.selectSecond(), PromiseOrigin.ALL)
                       .thenComposeAsync(vals -> parallelStep2(
                               values, step + 1, batchSize, 
                               accumulate(vals, step, current, downstream), 
                               spawner, downstream, downstreamExecutor
                               ), downstreamExecutor, true);
        }
    }
    
    private static <T> List<T> drainBatch(Iterator<? extends T> values, int batchSize) {
        List<T> valuesBatch = new ArrayList<>(batchSize);
        for (int count = 0; values.hasNext() && count < batchSize; count++) {
            valuesBatch.add(values.next());
        }        
        return valuesBatch;
    }
    
    private static <T, A, R> A accumulate(List<T> vals, int step, A current, Collector<T, A, R> downstream) {
        A insertion = downstream.supplier().get();
        vals.stream()
            .forEach(v -> downstream.accumulator().accept(insertion, v));
        
        return step == 0 ? insertion : downstream.combiner().apply(current, insertion);
    }

    
    /**
     * <p>Returns a promise that is resolved successfully when all {@link CompletionStage}-s passed as parameters
     * are completed normally; if any promise completed exceptionally, then resulting promise is resolved faulty
     * as well.
     * <p>The resolved result of this promise contains a list of the resolved results of the 
     * {@link CompletionStage}-s passed as an argument at corresponding positions.
     * <p>When resulting promise is resolved faulty, all remaining incomplete {@link CompletionStage}-s are 
     * cancelled.  
     * @param <T>
     *   a common supertype of the resulting values
     * @param promises
     *   an array of {@link CompletionStage}-s to combine
     * @return
     *   a combined promise
     */
    @SafeVarargs
    public static <T> Promise<List<T>> all(CompletionStage<? extends T>... promises) {
        return all(Arrays.asList(promises));
    }
    
    
    public static <T> Promise<List<T>> all(List<? extends CompletionStage<? extends T>> promises) {
        return all(true, promises);        
    }
    /**
     * <p>Returns a promise that is resolved successfully when all {@link CompletionStage}-s passed as parameters
     * are completed normally; if any promise completed exceptionally, then resulting promise is resolved faulty
     * as well.
     * <p>The resolved result of this promise contains a list of the resolved results of the 
     * {@link CompletionStage}-s passed as an argument at corresponding positions.
     * <p>When resulting promise is resolved faulty <em>and</em> <code>cancelRemaining</code> parameter is
     * <code>true</code>, all remaining incomplete {@link CompletionStage}-s are cancelled.  
     * @param <T>
     *   a common supertype of the resulting values
     * @param cancelRemaining
     *   when true and resulting promise is resolved faulty all incomplete {@link CompletionStage}-s are cancelled
     * @param promises
     *   an array of {@link CompletionStage}-s to combine
     * @return
     *   a combined promise
     */
    @SafeVarargs
    public static <T> Promise<List<T>> all(boolean cancelRemaining, CompletionStage<? extends T>... promises) {
        return all(cancelRemaining, Arrays.asList(promises));
    }

    public static <T> Promise<List<T>> all(boolean cancelRemaining, List<? extends CompletionStage<? extends T>> promises) {
        return atLeast(null != promises ? promises.size() : 0, 0, cancelRemaining, promises);
    }
    /**
     * <p>Returns a promise that is resolved successfully when any {@link CompletionStage} passed as parameters
     * is completed normally (race is possible); if all promises completed exceptionally, then resulting promise
     * is resolved faulty as well.
     * <p>The resolved result of this promise contains a value of the first resolved result of the 
     * {@link CompletionStage}-s passed as an argument.
     * <p>When resulting promise is resolved successfully, all remaining incomplete {@link CompletionStage}-s
     * are cancelled.
     * 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param promises
     *   an array of {@link CompletionStage}-s to combine
     * @return
     *   a combined promise
     */
    @SafeVarargs
    public static <T> Promise<T> any(CompletionStage<? extends T>... promises) {
        return any(Arrays.asList(promises));
    }

    public static <T> Promise<T> any(List<? extends CompletionStage<? extends T>> promises) {
        return any(true, promises);
    }
    /**
     * <p>Returns a promise that is resolved successfully when any {@link CompletionStage} passed as parameters
     * is completed normally (race is possible); if all promises completed exceptionally, then resulting promise 
     * is resolved faulty as well.
     * <p>The resolved result of this promise contains a value of the first resolved result of the 
     * {@link CompletionStage}-s passed as an argument.
     * <p>When resulting promise is resolved successfully <em>and</em> <code>cancelRemaining</code> parameter is
     * <code>true</code>, all remaining incomplete {@link CompletionStage}-s are cancelled.
     * 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param cancelRemaining
     *   when true and resulting promise is resolved faulty all incomplete {@link CompletionStage}-s are cancelled   
     * @param promises
     *   an array of {@link CompletionStage}-s to combine
     * @return
     *   a combined promise
     */
    @SafeVarargs
    public static <T> Promise<T> any(boolean cancelRemaining, CompletionStage<? extends T>... promises) {
        return any(cancelRemaining, Arrays.asList(promises));
    }
    
    public static <T> Promise<T> any(boolean cancelRemaining, List<? extends CompletionStage<? extends T>> promises) {
        int size = null == promises ? 0 : promises.size();
        switch (size) {
            case 0:
                return insufficientNumberOfArguments(1, 0);
            case 1:
                @SuppressWarnings("unchecked")
                CompletionStage<T> singleResult = (CompletionStage<T>) promises.get(0);
                return transform(singleResult, Function.identity(), Promises::wrapMultitargetException);
            default:
                return transform(
                    atLeast(1, size - 1, cancelRemaining, promises), 
                    Promises::extractFirstNonNull, Function.identity() /* DO NOT unwrap multitarget exception */
                );
        }
    }
    
    /**
     * <p>Returns a promise that is resolved successfully when any {@link CompletionStage} passed as parameters 
     * is completed normally (race is possible); if any promise completed exceptionally before first result is 
     * available, then resulting promise is resolved faulty as well (unlike non-Strict variant, where exceptions 
     * are ignored if result is available at all).
     * <p>The resolved result of this promise contains a value of the first resolved result of the 
     * {@link CompletionStage}-s passed as an argument.
     * <p>When resulting promise is resolved either successfully or faulty, all remaining incomplete 
     * {@link CompletionStage}-s are cancelled. 
     * <p>Unlike other methods to combine promises (any, all, atLeast, atLeastStrict), the {@link Promise} returns
     * from this method reports exact exception. All other methods wrap it to {@link MultitargetException}. 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param promises
     *   an array of {@link CompletionStage}-s to combine
     * @return
     *   a combined promise
     */
    @SafeVarargs
    public static <T> Promise<T> anyStrict(CompletionStage<? extends T>... promises) {
        return anyStrict(Arrays.asList(promises)); 
    }
    
    public static <T> Promise<T> anyStrict(List<? extends CompletionStage<? extends T>> promises) {
        return anyStrict(true, promises);        
    }

    /**
     * <p>Returns a promise that is resolved successfully when any {@link CompletionStage} passed as parameters
     *  is completed normally (race is possible); if any promise completed exceptionally before first result is
     *  available, then resulting promise is resolved faulty as well (unlike non-Strict variant, where exceptions
     *  are ignored if result is available at all).
     * <p>The resolved result of this promise contains a value of the first resolved result of the 
     * {@link CompletionStage}-s passed as an argument.
     * <p>When resulting promise is resolved either successfully or faulty <em>and</em> <code>cancelRemaining</code>
     * parameter is <code>true</code>, all remaining incomplete {@link CompletionStage}-s are cancelled. 
     * <p>Unlike other methods to combine promises (any, all, atLeast, atLeastStrict), the {@link Promise} returns 
     * from this method reports exact exception. All other methods wrap it to {@link MultitargetException}. 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param cancelRemaining
     *   when true and resulting promise is resolved faulty all incomplete {@link CompletionStage}-s are cancelled     
     * @param promises
     *   an array of {@link CompletionStage}-s to combine
     * @return
     *   a combined promise
     */
    @SafeVarargs
    public static <T> Promise<T> anyStrict(boolean cancelRemaining, CompletionStage<? extends T>... promises) {
        return anyStrict(cancelRemaining, Arrays.asList(promises));
    }
    
    public static <T> Promise<T> anyStrict(boolean cancelRemaining, List<? extends CompletionStage<? extends T>> promises) {
        int size = null == promises ? 0 : promises.size();
        switch (size) {
            case 0:
                return insufficientNumberOfArguments(1, 0);
            case 1:
                @SuppressWarnings("unchecked")
                CompletionStage<T> singleResult = (CompletionStage<T>) promises.get(0);
                return from(singleResult);
            default:
                return transform(
                    atLeast(1, 0, cancelRemaining, promises), 
                    Promises::extractFirstNonNull, Promises::unwrapMultitargetException
                );
        }
    }
    
    /**
     * <p>Generalization of the {@link Promises#any(CompletionStage...)} method.</p>
     * <p>Returns a promise that is resolved successfully when at least <code>minResultCount</code> of 
     * {@link CompletionStage}-s passed as parameters are completed normally (race is possible); if less than 
     * <code>minResultCount</code> of promises completed normally, then resulting promise is resolved faulty.
     * <p>The resolved result of this promise contains a list of the resolved results of the 
     * {@link CompletionStage}-s passed as an argument at corresponding positions. Non-completed or completed 
     * exceptionally promises have <code>null</code> values.
     * <p>When resulting promise is resolved successfully, all remaining incomplete {@link CompletionStage}-s 
     * are cancelled. 
     * 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param minResultsCount
     *   a minimum number of promises that should be completed normally to resolve resulting promise successfully 
     * @param promises
     *   an array of {@link CompletionStage}-s to combine 
     * @return
     *   a combined promise 
     */
    @SafeVarargs
    public static <T> Promise<List<T>> atLeast(int minResultsCount, CompletionStage<? extends T>... promises) {
        return atLeast(minResultsCount, Arrays.asList(promises));
    }
    
    public static <T> Promise<List<T>> atLeast(int minResultsCount, List<? extends CompletionStage<? extends T>> promises) {
        return atLeast(minResultsCount, true, promises);
    }

    /**
     * <p>Generalization of the {@link Promises#any(CompletionStage...)} method.</p>
     * <p>Returns a promise that is resolved successfully when at least <code>minResultCount</code> of 
     * {@link CompletionStage}-s passed as parameters are completed normally (race is possible); if less than 
     * <code>minResultCount</code> of promises completed normally, then resulting promise is resolved faulty.
     * <p>The resolved result of this promise contains a list of the resolved results of the 
     * {@link CompletionStage}-s passed as an argument at corresponding positions. Non-completed or completed 
     * exceptionally promises have <code>null</code> values.
     * <p>When resulting promise is resolved successfully <em>and</em> <code>cancelRemaining</code> parameter 
     * is <code>true</code>, all remaining incomplete {@link CompletionStage}-s are cancelled.  
     * 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param minResultsCount
     *   a minimum number of promises that should be completed normally to resolve resulting promise successfully 
     * @param cancelRemaining
     *   when true and resulting promise is resolved faulty all incomplete {@link CompletionStage}-s are cancelled    
     * @param promises
     *   an array of {@link CompletionStage}-s to combine 
     * @return
     *   a combined promise 
     */
    @SafeVarargs
    public static <T> Promise<List<T>> atLeast(int minResultsCount, boolean cancelRemaining, 
                                               CompletionStage<? extends T>... promises) {
        
        return atLeast(minResultsCount, cancelRemaining, Arrays.asList(promises));
    }
    
    public static <T> Promise<List<T>> atLeast(int minResultsCount, boolean cancelRemaining,
            
                                               List<? extends CompletionStage<? extends T>> promises) {
        return atLeast(minResultsCount, (promises == null ? 0 : promises.size()) - minResultsCount, cancelRemaining, promises);
    }
    
    /**
     * <p>Generalization of the {@link Promises#anyStrict(CompletionStage...)} method.</p>
     * <p>Returns a promise that is resolved successfully when at least <code>minResultCount</code> of 
     * {@link CompletionStage}-s passed as parameters are completed normally (race is possible); if less than 
     * <code>minResultCount</code> of promises completed normally, then resulting promise is resolved faulty. 
     * If any promise completed exceptionally <em>before</em> <code>minResultCount</code> of results are 
     * available, then resulting promise is resolved faulty as well. 
     * <p>The resolved result of this promise contains a list of the resolved results of the 
     * {@link CompletionStage}-s passed as an argument at corresponding positions. Non-completed promises 
     * have <code>null</code> values.
     * <p>When resulting promise is resolved either successfully or faulty, all remaining incomplete 
     * {@link CompletionStage}-s are cancelled.
     *  
     * @param <T>
     *   a common supertype of the resulting values 
     * @param minResultsCount
     *   a minimum number of promises that should be completed normally to resolve resulting promise successfully 
     * @param promises
     *   an array of {@link CompletionStage}-s to combine 
     * @return
     *   a combined promise 
     */    
    @SafeVarargs
    public static <T> Promise<List<T>> atLeastStrict(int minResultsCount, CompletionStage<? extends T>... promises) {
        return atLeastStrict(minResultsCount, Arrays.asList(promises));
    }
    
    public static <T> Promise<List<T>> atLeastStrict(int minResultsCount, 
                                                     List<? extends CompletionStage<? extends T>> promises) {
        return atLeastStrict(minResultsCount, true, promises);
    }

    /**
     * <p>Generalization of the {@link Promises#anyStrict(CompletionStage...)} method.</p>
     * <p>Returns a promise that is resolved successfully when at least <code>minResultCount</code> of 
     * {@link CompletionStage}-s passed as parameters are completed normally (race is possible); if less than 
     * <code>minResultCount</code> of promises completed normally, then resulting promise is resolved faulty. 
     * If any promise completed exceptionally <em>before</em> <code>minResultCount</code> of results are available, 
     * then resulting promise is resolved faulty as well. 
     * <p>The resolved result of this promise contains a list of the resolved results of the 
     * {@link CompletionStage}-s passed as an argument at corresponding positions. Non-completed promises have 
     * <code>null</code> values.
     * <p>When resulting promise is resolved either successfully or faulty <em>and</em> <code>cancelRemaining</code> 
     * parameter is <code>true</code>, all remaining incomplete {@link CompletionStage}-s are cancelled.
     *  
     * @param <T>
     *   a common supertype of the resulting values 
     * @param minResultsCount
     *   a minimum number of promises that should be completed normally to resolve resulting promise successfully 
     * @param cancelRemaining
     *   when true and resulting promise is resolved faulty all incomplete {@link CompletionStage}-s are cancelled     
     * @param promises
     *   an array of {@link CompletionStage}-s to combine 
     * @return
     *   a combined promise 
     */    
    @SafeVarargs
    public static <T> Promise<List<T>> atLeastStrict(int minResultsCount, boolean cancelRemaining, 
                                                     CompletionStage<? extends T>... promises) {
        return atLeast(minResultsCount, cancelRemaining, Arrays.asList(promises));
    }    
    
    public static <T> Promise<List<T>> atLeastStrict(int minResultsCount, boolean cancelRemaining, 
                                                     List<? extends CompletionStage<? extends T>> promises) {
        return atLeast(minResultsCount, 0, cancelRemaining, promises);
    }
    
    /**
     * <p>General method to combine several {@link CompletionStage}-s passed as arguments into single promise.</p>
     * <p>The resulting promise is resolved successfully when at least <code>minResultCount</code> of 
     * {@link CompletionStage}-s passed as parameters are completed normally (race is possible).
     * <p>If less than <code>minResultCount</code> of promises completed normally, then resulting promise is 
     * resolved faulty. 
     * <p>If <code>maxErrorsCount</code> of promises completed exceptionally <em>before</em> <code>minResultCount</code> 
     * of results are available, then resulting promise is resolved faulty as well. 
     * <p>The resolved result of this promise contains a list of the resolved results of the {@link CompletionStage}-s 
     * passed as an argument at corresponding positions. Non-completed promises and promises completed exceptionally 
     * have <code>null</code> values.
     * <p>When resulting promise is resolved either successfully or faulty, all remaining incomplete 
     * {@link CompletionStage}-s are cancelled <em>if</em> <code>cancelRemaining</code> parameter is <code>true</code>. 
     * 
     * @param <T>
     *   a common supertype of the resulting values 
     * @param minResultsCount
     *   a minimum number of promises that should be completed normally to resolve resulting promise successfully
     * @param maxErrorsCount
     *   a maximum number of promises that may be completed exceptionally before resolving resulting promise faulty
     * @param cancelRemaining
     *   a flag that indicates (if true) whether or not all remaining incomplete {@link CompletionStage}-s should 
     *   be cancelled once a resulting promise outcome is known. 
     * @param promises
     *   an array of {@link CompletionStage}-s to combine 
     * @return
     *   a combined promise 
     */    
    @SafeVarargs
    public static <T> Promise<List<T>> atLeast(int minResultsCount, int maxErrorsCount, boolean cancelRemaining, 
                                               CompletionStage<? extends T>... promises) {
        
        return atLeast(minResultsCount, maxErrorsCount, cancelRemaining, Arrays.asList(promises));
    }
    
    public static <T> Promise<List<T>> atLeast(int minResultsCount, int maxErrorsCount, boolean cancelRemaining, 
                                               List<? extends CompletionStage<? extends T>> promises) {
        
        int size = null == promises ? 0 : promises.size();
        if (minResultsCount > size) {
            Promise<List<T>> result = insufficientNumberOfArguments(minResultsCount, size);
            if (cancelRemaining && size > 0) {
                promises.stream().forEach( p -> cancelPromise(p, true) );
            }
            return result;
        } else if (minResultsCount == 0) {
            return success(Collections.emptyList());
        } else if (size == 1) {
            CompletionStage<? extends T> stage = promises.get(0);
            return transform(stage, Collections::singletonList, Promises::wrapMultitargetException);
        } else {
            return new AggregatingPromise<>(minResultsCount, maxErrorsCount, cancelRemaining, promises);
        }
    }
    
    public static Promise<Void> retry(Runnable codeBlock, Executor executor, 
                                      RetryPolicy<? super Void> retryPolicy) {
        
        return retry(ctx -> { codeBlock.run(); }, executor, retryPolicy);
    }
    
    public static Promise<Void> retry(RetryRunnable codeBlock, Executor executor, 
                                      RetryPolicy<? super Void> retryPolicy) {
        
        return retry(ctx -> { codeBlock.run(ctx); return null; }, executor, retryPolicy.acceptNullResult());
    }

    public static <T> Promise<T> retry(Callable<T> codeBlock, Executor executor, 
                                       RetryPolicy<? super T> retryPolicy) {
        
        return retry(toRetryCallable(codeBlock), executor, retryPolicy);
    }

    public static <T extends C, C> Promise<T> retry(RetryCallable<T, C> codeBlock, Executor executor, 
                                                    RetryPolicy<? super C> retryPolicy) {
        
        return startRetry(retryPolicy, new RetryInitiator<T, C>() {
            @Override
            public void run(RetryContext<C> ctx, CompletableFuture<T> result, Consumer<Promise<?>> cancellation) {
                tryValueOnce(codeBlock, executor, ctx, result, cancellation);
            }
        }).defaultAsyncOn(executor);        
    }
    
    public static <T> Promise<T> retryOptional(Callable<Optional<T>> codeBlock, Executor executor, 
                                               RetryPolicy<? super T> retryPolicy) {
        
        return retryOptional(toRetryCallable(codeBlock), executor, retryPolicy);
    }
    
    public static <T extends C, C> Promise<T> retryOptional(RetryCallable<Optional<T>, C> codeBlock, Executor executor, 
                                                            RetryPolicy<? super C> retryPolicy) {
        
        // Need explicit type on lambda param
        return retry((RetryContext<C> ctx) -> codeBlock.call(ctx).orElse(null), executor, retryPolicy);
    }
    
    public static <T> Promise<T> retryFuture(Callable<? extends CompletionStage<T>> invoker, 
                                             RetryPolicy<? super T> retryPolicy) {
        
        return retryFuture(toRetryCallable(invoker), retryPolicy);
    }
    
    public static <T extends C, C> Promise<T> retryFuture(RetryCallable<? extends CompletionStage<T>, C> futureFactory, 
                                                          RetryPolicy<? super C> retryPolicy) {
        
        return startRetry(retryPolicy, new RetryInitiator<T, C>() {
            @Override
            public void run(RetryContext<C> ctx, CompletableFuture<T> result, Consumer<Promise<?>> cancellation) {
                tryFutureOnce(futureFactory, ctx, result, cancellation, null);
            }
        });
    }
    
    private static <T extends C, C> Promise<T> startRetry(RetryPolicy<? super C> retryPolicy, RetryInitiator<T, C> initiator) {
        
        final CompletableFuture<T> result = new CompletableFuture<>();
        final AtomicReference<Promise<?>> callPromiseRef = new AtomicReference<>();
        // Cleanup latest timeout on completion;
        result.whenComplete(
            (r, e) -> 
                Optional
                .of(callPromiseRef)
                .map(AtomicReference::get)
                .ifPresent( p -> p.cancel(true) )
        );
        Consumer<Promise<?>> cancellation = p -> {
            // If result promise is cancelled after callPromise was set need to stop;               
            callPromiseRef.set( p );    
            if (result.isDone()) {
                p.cancel(true);
            }
        };
        
        RetryContext<C> ctx = RetryContext.initial(retryPolicy);     
        initiator.run(ctx, result, cancellation);
        return new CompletableFutureWrapper<>(result);
    }
    
    private static <T extends C, C> void tryValueOnce(RetryCallable<T, C> codeBlock, 
                                                      Executor executor, 
                                                      RetryContext<C> ctx, 
                                                      CompletableFuture<T> result, 
                                                      Consumer<Promise<?>> cancellation) {
        
        // Promise may be cancelled outside of polling
        if (result.isDone()) {
            return;
        }
        
        RetryPolicy.Verdict verdict = ctx.shouldContinue();
        if (verdict.shouldExecute()) {
            Supplier<Promise<?>> callSupplier = () -> {
                long startTime = System.nanoTime();                
                Runnable call = () -> {
                    try {
                        T value = codeBlock.call(ctx);
                        if (ctx.isValidResult(value)) {
                            result.complete(value);
                        } else {
                            long finishTime = System.nanoTime();
                            RetryContext<C> nextCtx = ctx.nextRetry(duration(startTime, finishTime), value);
                            tryValueOnce(codeBlock, executor, nextCtx, result, cancellation);
                        }
                    } catch (Exception ex) {
                        long finishTime = System.nanoTime();
                        RetryContext<C> nextCtx = ctx.nextRetry(duration(startTime, finishTime), ex);
                        tryValueOnce(codeBlock, executor, nextCtx, result, cancellation);
                    }
                };
                
                // Call should be done via CompletableTask to let it be interruptible               
            	Promise<?> p = CompletableTask.runAsync(call, executor);
            	return applyExecutionTimeout(p, verdict);
            };
            Duration backoffDelay = verdict.backoffDelay();
            if (DelayPolicy.isValid(backoffDelay)) {
                // Invocation after timeout, change cancellation target
                Promise<?> later = Timeouts
                    .delay(backoffDelay)
                    .dependent()
                    .thenRun(() -> cancellation.accept( callSupplier.get() ), true);
                cancellation.accept( later );
            } else {
                // Immediately send to executor
                cancellation.accept( callSupplier.get() ); 
            } 
        } else {
            result.completeExceptionally(ctx.asFailure());
        }
    }
    
    private static <T extends C, C> void tryFutureOnce(RetryCallable<? extends CompletionStage<T>, C> futureFactory, 
                                                       RetryContext<C> ctx, 
                                                       CompletableFuture<T> result,
                                                       Consumer<Promise<?>> cancellation,
                                                       Promise<?> prev) {
        // Promise may be cancelled outside of polling
        if (result.isDone()) {
            return;
        }
        
        RetryPolicy.Verdict verdict = ctx.shouldContinue();
        if (verdict.shouldExecute()) {
            Supplier<Promise<?>> callSupplier = () -> {
                long startTime = System.nanoTime();
                
                Promise<? extends T> target;
                try {
                    target = Promises.from(futureFactory.call(ctx));
                } catch (Exception ex) {
                    target = Promises.failure(ex);
                }

                AtomicBoolean isRecursive = new AtomicBoolean(true);
                Thread invokerThread = Thread.currentThread();

                Promise<? extends T> p = target;
                p.whenComplete((value, ex) -> {
                    if (null == ex && ctx.isValidResult(value)) {
                        result.complete(value);
                    } else {
                        long finishTime = System.nanoTime();
                        RetryContext<C> nextCtx = ctx.nextRetry(
                            duration(startTime, finishTime), SharedFunctions.unwrapCompletionException(ex)
                        );
                        boolean callLater = isRecursive.get() && Thread.currentThread() == invokerThread;
                        if (callLater) {
                            // Call after minimal possible delay
                            callLater(
                                p, MINIMAL_DELAY, cancellation, 
                                () -> tryFutureOnce(futureFactory, nextCtx, result, cancellation, p)
                            );
                        } else {
                            tryFutureOnce(futureFactory, nextCtx, result, cancellation, p);
                        }
                    }
                });
                isRecursive.set(false);
                return applyExecutionTimeout(p, verdict);
            };
            Duration backoffDelay = verdict.backoffDelay();
            if (null != prev && DelayPolicy.isValid(backoffDelay)) {
                callLater(prev, backoffDelay, cancellation, () -> cancellation.accept( callSupplier.get() ));
            } else {
                // Immediately send to executor
                cancellation.accept( callSupplier.get() ); 
            }  
        } else {
            result.completeExceptionally(ctx.asFailure());
        }        
    }
    
    private static <T> void callLater(Promise<T> completedPromise, Duration delay, Consumer<Promise<?>> cancellation, Runnable code) {
        Promise<?> later = completedPromise
            .dependent()
            .thenCombine(Timeouts.delay(delay), selectFirst(), PromiseOrigin.PARAM_ONLY)
            .whenCompleteAsync((r, e) -> code.run(), true);
        cancellation.accept(later);
    }

    private static <T> Promise<T> applyExecutionTimeout(Promise<T> singleInvocationPromise, RetryPolicy.Verdict verdict) {
        Duration timeout = verdict.timeout();
        if (DelayPolicy.isValid(timeout)) {
            singleInvocationPromise.dependent().orTimeout( timeout, true, true ).unwrap();
        }
        return singleInvocationPromise;        
    }
    
    private static <T, U> Promise<T> transform(CompletionStage<U> original, 
                                               Function<? super U, ? extends T> resultMapper, 
                                               Function<? super Throwable, ? extends Throwable> errorMapper) {
        CompletablePromise<T> result = new CompletablePromise<>();
        original.whenComplete((r, e) -> {
           if (null == e) {
               result.onSuccess( resultMapper.apply(r) );
           } else {
               result.onFailure( errorMapper.apply(e) );
           }
        });
        return result.onCancel(() -> cancelPromise(original, true));
    }
    
    private static <T> T extractFirstNonNull(Collection<? extends T> collection) {
        return collection.stream().filter(Objects::nonNull).findFirst().get();
    }
    
    private static <E extends Throwable> Throwable unwrapMultitargetException(E exception) {
        Throwable targetException = SharedFunctions.unwrapCompletionException(exception);
        if (targetException instanceof MultitargetException) {
            return ((MultitargetException)targetException).getFirstException().get();
        } else {
            return targetException;
        }
    }
    
    private static <E extends Throwable> MultitargetException wrapMultitargetException(E exception) {
        if (exception instanceof MultitargetException) {
            return (MultitargetException)exception;
        } else {
            return MultitargetException.of(exception);
        }
    }
    
    private static <T> Promise<T> insufficientNumberOfArguments(int minResultCount, int size) {
        String message = String.format(
            "The number of futures supplied (%d) is less than a number of futures to await (%d)", size, minResultCount
        );
        Exception ex = new NoSuchElementException(message);
        //TODO: exceptional completion vs runtime exception on combined promise construction?
        ex.fillInStackTrace();
        return failure(ex);
        /*
        throw new IllegalArgumentException(message);        
        */
    }
    
    private static <V, T> RetryCallable<V, T> toRetryCallable(Callable<? extends V> callable) {
        return ctx -> callable.call();
    }
    
    private static Duration duration(long startTime, long finishTime) {
        return Duration.ofNanos(finishTime - startTime);
    }
    
    private static abstract class RetryInitiator<T extends C, C> {
        abstract void run(RetryContext<C> ctx, CompletableFuture<T> result, Consumer<Promise<?>> cancellation);
    }

    private static final Duration MINIMAL_DELAY = Duration.ofNanos(1L);
}