package com.github.phantomthief.concurrent;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly;
import static java.lang.System.nanoTime;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

import java.time.Duration;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.phantomthief.util.ThrowableConsumer;
import com.github.phantomthief.util.ThrowableFunction;
import com.github.phantomthief.util.ThrowableRunnable;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.ExecutionError;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.common.util.concurrent.UncheckedTimeoutException;

/**
 * @author w.vela
 * Created on 2018-06-25.
 */
public class MoreFutures {

    private static final Logger logger = LoggerFactory.getLogger(MoreFutures.class);

    /**
     * @throws UncheckedTimeoutException if timeout occurred.
     * @throws java.util.concurrent.CancellationException if task was canceled.
     * @throws ExecutionError if a {@link Error} occurred.
     * @throws UncheckedExecutionException if a normal Exception occurred.
     */
    public static <T> T getUnchecked(@Nonnull Future<? extends T> future,
            @Nonnull Duration duration) {
        checkNotNull(duration);
        return getUnchecked(future, duration.toNanos(), NANOSECONDS);
    }

    /**
     * @throws UncheckedTimeoutException if timeout occurred.
     * @throws java.util.concurrent.CancellationException if task was canceled.
     * @throws ExecutionError if a {@link Error} occurred.
     * @throws UncheckedExecutionException if a normal Exception occurred.
     */
    public static <T> T getUnchecked(@Nonnull Future<? extends T> future, @Nonnegative long timeout,
            @Nonnull TimeUnit unit) {
        return getUnchecked(future, timeout, unit, false);
    }

    /**
     * @throws UncheckedTimeoutException if timeout occurred.
     * @throws java.util.concurrent.CancellationException if task was canceled.
     * @throws ExecutionError if a {@link Error} occurred.
     * @throws UncheckedExecutionException if a normal Exception occurred.
     */
    public static <T> T getUnchecked(@Nonnull Future<? extends T> future, @Nonnegative long timeout,
            @Nonnull TimeUnit unit, boolean cancelOnTimeout) {
        checkArgument(timeout > 0);
        checkNotNull(future);
        try {
            return getUninterruptibly(future, timeout, unit);
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof Error) {
                throw new ExecutionError((Error) cause);
            } else {
                throw new UncheckedExecutionException(cause);
            }
        } catch (TimeoutException e) {
            if (cancelOnTimeout) {
                future.cancel(false);
            }
            throw new UncheckedTimeoutException(e);
        }
    }

    /**
     * @see #tryWait(Iterable, long, TimeUnit)
     *
     * @throws TryWaitFutureUncheckedException if not all calls are successful.
     */
    @Nonnull
    public static <F extends Future<V>, V> Map<F, V> tryWait(@Nonnull Iterable<F> futures,
            @Nonnull Duration duration) throws TryWaitFutureUncheckedException {
        checkNotNull(futures);
        checkNotNull(duration);
        return tryWait(futures, duration.toNanos(), NANOSECONDS);
    }

    /**
     * A typical usage:
     * {@code <pre>
     *  // a fail-safe example
     *  List<Future<User>> list = doSomeAsyncTasks();
     *  Map<Future<User>, User> success;
     *  try {
     *    success = tryWait(list, 1, SECONDS);
     *  } catch (TryWaitUncheckedException e) {
     *    success = e.getSuccess(); // there are still some success
     *  }
     *
     *  // a fail-fast example
     *  List<Future<User>> list = doSomeAsyncTasks();
     *  // don't try/catch the exception it throws.
     *  Map<Future<User>, User> success = tryWait(list, 1, SECONDS);
     * </pre>}
     *
     * @throws TryWaitUncheckedException if not all calls are successful.
     */
    @Nonnull
    public static <F extends Future<V>, V> Map<F, V> tryWait(@Nonnull Iterable<F> futures,
            @Nonnegative long timeout, @Nonnull TimeUnit unit)
            throws TryWaitFutureUncheckedException {
        checkNotNull(futures);
        checkArgument(timeout > 0);
        checkNotNull(unit);
        return tryWait(futures, timeout, unit, it -> it, TryWaitFutureUncheckedException::new);
    }

    /**
     * @see #tryWait(Iterable, long, TimeUnit, ThrowableFunction)
     *
     * @throws TryWaitUncheckedException if not all calls are successful.
     */
    @Nonnull
    public static <K, V, X extends Throwable> Map<K, V> tryWait(@Nonnull Iterable<K> keys,
            @Nonnull Duration duration, @Nonnull ThrowableFunction<K, Future<V>, X> asyncFunc)
            throws X, TryWaitUncheckedException {
        checkNotNull(keys);
        checkNotNull(duration);
        checkNotNull(asyncFunc);
        return tryWait(keys, duration.toNanos(), NANOSECONDS, asyncFunc);
    }

    /**
     * A typical usage:
     * {@code <pre>
     *  // a fail-safe example
     *  List<Integer> list = getSomeIds();
     *  Map<Integer, User> success;
     *  try {
     *    success = tryWait(list, 1, SECONDS, id -> executor.submit(() -> retrieve(id)));
     *  } catch (TryWaitUncheckedException e) {
     *    success = e.getSuccess(); // there are still some success
     *  }
     *
     *  // a fail-fast example
     *  List<Integer> list = getSomeIds();
     *  // don't try/catch the exception it throws.
     *  Map<Integer, User> success = tryWait(list, 1, SECONDS, id -> executor.submit(() -> retrieve(id)));
     * </pre>}
     *
     * @throws TryWaitUncheckedException if not all calls are successful.
     */
    @Nonnull
    public static <K, V, X extends Throwable> Map<K, V> tryWait(@Nonnull Iterable<K> keys,
            @Nonnegative long timeout, @Nonnull TimeUnit unit,
            @Nonnull ThrowableFunction<K, Future<V>, X> asyncFunc)
            throws X, TryWaitUncheckedException {
        return tryWait(keys, timeout, unit, asyncFunc, TryWaitUncheckedException::new);
    }

    @Nonnull
    private static <K, V, X extends Throwable> Map<K, V> tryWait(@Nonnull Iterable<K> keys,
            @Nonnegative long timeout, @Nonnull TimeUnit unit,
            @Nonnull ThrowableFunction<K, Future<V>, X> asyncFunc,
            @Nonnull Function<TryWaitResult, RuntimeException> throwing) throws X {
        checkNotNull(keys);
        checkArgument(timeout > 0);
        checkNotNull(unit);
        checkNotNull(asyncFunc);

        Map<Future<? extends V>, V> successMap = new LinkedHashMap<>();
        Map<Future<? extends V>, Throwable> failMap = new LinkedHashMap<>();
        Map<Future<? extends V>, TimeoutException> timeoutMap = new LinkedHashMap<>();
        Map<Future<? extends V>, CancellationException> cancelMap = new LinkedHashMap<>();

        long remainingNanos = unit.toNanos(timeout);
        long end = nanoTime() + remainingNanos;

        Map<Future<? extends V>, K> futureKeyMap = new IdentityHashMap<>();
        for (K key : keys) {
            checkNotNull(key);
            Future<V> future = asyncFunc.apply(key);
            checkNotNull(future);
            futureKeyMap.put(future, key);
        }
        for (Future<? extends V> future : futureKeyMap.keySet()) {
            if (remainingNanos <= 0) {
                waitAndCollect(successMap, failMap, timeoutMap, cancelMap, future, 1L);
                continue;
            }
            waitAndCollect(successMap, failMap, timeoutMap, cancelMap, future, remainingNanos);
            remainingNanos = end - nanoTime();
        }

        TryWaitResult<K, V> result = new TryWaitResult<>(successMap, failMap, timeoutMap, cancelMap,
                futureKeyMap);

        if (failMap.isEmpty() && timeoutMap.isEmpty() && cancelMap.isEmpty()) {
            return result.getSuccess();
        } else {
            throw throwing.apply(result);
        }
    }

    private static <T> void waitAndCollect(Map<Future<? extends T>, T> successMap,
            Map<Future<? extends T>, Throwable> failMap,
            Map<Future<? extends T>, TimeoutException> timeoutMap,
            Map<Future<? extends T>, CancellationException> cancelMap, Future<? extends T> future,
            long thisWait) {
        try {
            T t = getUninterruptibly(future, thisWait, NANOSECONDS);
            successMap.put(future, t);
        } catch (CancellationException e) {
            cancelMap.put(future, e);
        } catch (TimeoutException e) {
            timeoutMap.put(future, e);
        } catch (ExecutionException e) {
            failMap.put(future, e.getCause());
        } catch (Throwable e) {
            failMap.put(future, e);
        }
    }

    /**
     * @param task any exception throwing would cancel the task. user should swallow exceptions by self.
     * @param executor all task would be stopped after executor has been marked shutting down.
     * @return a future that can cancel the task.
     */
    public static Future<?> scheduleWithDynamicDelay(@Nonnull ScheduledExecutorService executor,
            @Nullable Duration initDelay, @Nonnull Scheduled task) {
        checkNotNull(executor);
        checkNotNull(task);
        AtomicBoolean canceled = new AtomicBoolean(false);
        AbstractFuture<?> future = new AbstractFuture<Object>() {

            @Override
            public boolean cancel(boolean mayInterruptIfRunning) {
                canceled.set(true);
                return super.cancel(mayInterruptIfRunning);
            }
        };
        executor.schedule(new ScheduledTaskImpl(executor, task, canceled),
                initDelay == null ? 0 : initDelay.toMillis(), MILLISECONDS);
        return future;
    }

    /**
     * @param task any exception throwing would be ignore and logged, task would not cancelled.
     * @param executor all task would be stopped after executor has been marked shutting down.
     * @return a future that can cancel the task.
     */
    public static Future<?> scheduleWithDynamicDelay(@Nonnull ScheduledExecutorService executor,
            @Nonnull Duration initialDelay, @Nonnull Supplier<Duration> delay,
            @Nonnull ThrowableRunnable<Throwable> task) {
        checkNotNull(initialDelay);
        checkNotNull(delay);
        checkNotNull(task);
        return scheduleWithDynamicDelay(executor, initialDelay, () -> {
            try {
                task.run();
            } catch (Throwable e) {
                logger.error("", e);
            }
            return delay.get();
        });
    }

    /**
     * @param task any exception throwing would be ignore and logged, task would not cancelled.
     * @param executor all task would be stopped after executor has been marked shutting down.
     * @return a future that can cancel the task.
     */
    public static Future<?> scheduleWithDynamicDelay(@Nonnull ScheduledExecutorService executor,
            @Nonnull Supplier<Duration> delay, @Nonnull ThrowableRunnable<Throwable> task) {
        checkNotNull(delay);
        return scheduleWithDynamicDelay(executor, delay.get(), () -> {
            try {
                task.run();
            } catch (Throwable e) {
                logger.error("", e);
            }
            return delay.get();
        });
    }

    /**
     * 用于替换 {@link Futures#transform(ListenableFuture, com.google.common.base.Function, Executor)}
     * <p>
     * 主要提供两个额外的功能:
     * 1. API使用jdk8
     * 2. 提供了 {@link TimeoutListenableFuture} 的支持(保持Listener不会丢)
     */
    public static <I, O> ListenableFuture<O> transform(ListenableFuture<I> input,
            Function<? super I, ? extends O> function, Executor executor) {
        @SuppressWarnings("Guava")
        com.google.common.base.Function<? super I, ? extends O> realFunc;
        if (function instanceof com.google.common.base.Function) {
            //noinspection unchecked
            realFunc = (com.google.common.base.Function) function;
        } else {
            realFunc = function::apply;
        }
        ListenableFuture<O> result = Futures.transform(input, realFunc, executor);
        if (input instanceof TimeoutListenableFuture) {
            TimeoutListenableFuture<O> newResult = new TimeoutListenableFuture<>(result);
            for (ThrowableConsumer<TimeoutException, Exception> timeoutListener : ((TimeoutListenableFuture<I>) input)
                    .getTimeoutListeners()) {
                newResult.addTimeoutListener(timeoutListener);
            }
            return newResult;
        } else {
            return result;
        }
    }

    public static <I, O> ListenableFuture<O> transformAsync(ListenableFuture<I> input,
            AsyncFunction<? super I, ? extends O> function,
            Executor executor) {
        ListenableFuture<O> result = Futures.transformAsync(input, function, executor);
        if (input instanceof TimeoutListenableFuture) {
            TimeoutListenableFuture<O> newResult = new TimeoutListenableFuture<>(result);
            for (ThrowableConsumer<TimeoutException, Exception> timeoutListener : ((TimeoutListenableFuture<I>) input)
                    .getTimeoutListeners()) {
                newResult.addTimeoutListener(timeoutListener);
            }
            return newResult;
        } else {
            return result;
        }
    }

    public interface Scheduled {

        /**
         * @return a delay for next run. {@code null} means stop.
         */
        @Nullable
        Duration run();
    }

    private static class ScheduledTaskImpl implements Runnable {

        private final ScheduledExecutorService executorService;
        private final Scheduled scheduled;
        private final AtomicBoolean canceled;

        private ScheduledTaskImpl(ScheduledExecutorService executorService, Scheduled scheduled,
                AtomicBoolean canceled) {
            this.executorService = executorService;
            this.scheduled = scheduled;
            this.canceled = canceled;
        }

        @Override
        public void run() {
            if (canceled.get()) {
                return;
            }
            try {
                Duration delay = scheduled.run();
                if (!canceled.get() && delay != null) {
                    executorService.schedule(this, delay.toMillis(), MILLISECONDS);
                }
            } catch (Throwable e) {
                logger.error("", e);
            }
        }
    }
}