/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 com.android.camera.async;

import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.ForwardingListenableFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;

import java.util.List;

import javax.annotation.Nullable;

/**
 * TODO Replace with Guava's com.google.common.util.concurrent.Futures once we
 * can build with v. 15+
 */
public class Futures2 {
    /**
     * 2 parameter async function for joined async results.
     */
    public interface AsyncFunction2<T1, T2, TResult> {
        ListenableFuture<TResult> apply(T1 value1, T2 value2) throws Exception;
    }

    /**
     * 3 parameter async function for joined async results.
     */
    public interface AsyncFunction3<T1, T2, T3, TResult> {
        ListenableFuture<TResult> apply(T1 value1, T2 value2, T3 value3) throws Exception;
    }

    /**
     * 2 parameter function for joining multiple results into a single value.
     */
    public interface Function2<T1, T2, TResult> {
        TResult apply(T1 value1, T2 value2);
    }

    /**
     * 3 parameter function for joining multiple results into a single value.
     */
    public interface Function3<T1, T2, T3, TResult> {
        TResult apply(T1 value1, T2 value2, T3 value3);
    }

    private Futures2() {
    }

    /**
     * Creates a new ListenableFuture whose result is set from the supplied
     * future when it completes. Cancelling the supplied future will also cancel
     * the returned future, but cancelling the returned future will have no
     * effect on the supplied future.
     */
    public static <T> ListenableFuture<T> nonCancellationPropagating(
            final ListenableFuture<T> future) {
        return new ForwardingListenableFuture.SimpleForwardingListenableFuture<T>(future) {
            @Override
            public boolean cancel(boolean mayInterruptIfNecessary) {
                return false;
            }
        };
    }

    /**
     * Create a new joined future from two existing futures and a joining function
     * that combines the resulting outputs of the previous functions into a single
     * result. The resulting future will fail if any of the dependent futures also
     * fail.
     */
    public static <T1, T2, TResult> ListenableFuture<TResult> joinAll(
          final ListenableFuture<T1> f1,
          final ListenableFuture<T2> f2,
          final AsyncFunction2<T1, T2, TResult> fn) {
        ListenableFuture<?>[] futures = new ListenableFuture<?>[2];

        futures[0] = f1;
        futures[1] = f2;

        // Futures.allAsList is used instead of Futures.successfulAsList because
        // allAsList will propagate the failures instead of null values to the
        // parameters of the supplied function.
        ListenableFuture<List<Object>> result = Futures.<Object>allAsList(futures);
        return Futures.transformAsync(result, new AsyncFunction<List<Object>, TResult>() {
            @Override
            public ListenableFuture<TResult> apply(@Nullable List<Object> list) throws Exception {
                T1 value1 = (T1) list.get(0);
                T2 value2 = (T2) list.get(1);

                return fn.apply(value1, value2);
            }
        }, MoreExecutors.directExecutor());
    }

    /**
     * Create a new joined future from two existing futures and an async function
     * that combines the resulting outputs of the previous functions into a single
     * result. The resulting future will fail if any of the dependent futures also
     * fail.
     */
    public static <T1, T2, TResult> ListenableFuture<TResult> joinAll(
          final ListenableFuture<T1> f1,
          final ListenableFuture<T2> f2,
          final Function2<T1, T2, TResult> fn) {
        return joinAll(f1, f2, new ImmediateAsyncFunction2<>(fn));
    }

    /**
     * Create a new joined future from three existing futures and a joining function
     * that combines the resulting outputs of the previous functions into a single
     * result. The resulting future will fail if any of the dependent futures also
     * fail.
     */
    public static <T1, T2, T3, TResult> ListenableFuture<TResult> joinAll(
          final ListenableFuture<T1> f1,
          final ListenableFuture<T2> f2,
          final ListenableFuture<T3> f3,
          final AsyncFunction3<T1, T2, T3, TResult> fn) {
        ListenableFuture<?>[] futures = new ListenableFuture<?>[3];

        futures[0] = f1;
        futures[1] = f2;
        futures[2] = f3;

        // Futures.allAsList is used instead of Futures.successfulAsList because
        // allAsList will propagate the failures instead of null values to the
        // parameters of the supplied function.
        ListenableFuture<List<Object>> result = Futures.<Object>allAsList(futures);
        return Futures.transformAsync(result, new AsyncFunction<List<Object>, TResult>() {
            @Override
            public ListenableFuture<TResult> apply(@Nullable List<Object> list) throws Exception {
                T1 value1 = (T1) list.get(0);
                T2 value2 = (T2) list.get(1);
                T3 value3 = (T3) list.get(2);

                return fn.apply(value1, value2, value3);
            }
        }, MoreExecutors.directExecutor());
    }

    /**
     * Create a new joined future from three existing futures and an async function
     * that combines the resulting outputs of the previous functions into a single
     * result. The resulting future will fail if any of the dependent futures also
     * fail.
     */
    public static <T1, T2, T3, TResult> ListenableFuture<TResult> joinAll(
          final ListenableFuture<T1> f1,
          final ListenableFuture<T2> f2,
          final ListenableFuture<T3> f3,
          final Function3<T1, T2, T3, TResult> fn) {
        return joinAll(f1, f2, f3, new ImmediateAsyncFunction3<>(fn));
    }

    /**
     * Wrapper class for turning a Function2 into an AsyncFunction2 by returning
     * an immediate future when the function is applied.
     */
    private static final class ImmediateAsyncFunction2<T1, T2, TResult> implements
          AsyncFunction2<T1, T2, TResult> {
        private final Function2<T1, T2, TResult> mFn;

        public ImmediateAsyncFunction2(Function2<T1, T2, TResult> fn) {
            mFn = fn;
        }

        @Override
        public ListenableFuture<TResult> apply(T1 value1, T2 value2) throws Exception {
            return Futures.immediateFuture(mFn.apply(value1, value2));
        }
    }

    /**
     * Wrapper class for turning a Function3 into an AsyncFunction3 by returning
     * an immediate future when the function is applied.
     */
    private static final class ImmediateAsyncFunction3<T1, T2, T3, TResult> implements
          AsyncFunction3<T1, T2, T3, TResult> {
        private final Function3<T1, T2, T3, TResult> mFn;

        public ImmediateAsyncFunction3(Function3<T1, T2, T3, TResult> fn) {
            mFn = fn;
        }

        @Override
        public ListenableFuture<TResult> apply(T1 value1, T2 value2, T3 value3) throws Exception {
            return Futures.immediateFuture(mFn.apply(value1, value2, value3));
        }
    }
}