/*
 * Copyright (C) 2021 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 android.car.util.concurrent;

import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;

import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Slog;

import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;

import java.lang.reflect.Constructor;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * code copied from {@code com.android.internal.infra.AndroidFuture}
 *
 * @param <T> see {@link CompletableFuture}
 *
 * @hide
 */
public class AndroidFuture<T> extends CompletableFuture<T> implements Parcelable {

    private static final boolean DEBUG = false;
    private static final String LOG_TAG = AndroidFuture.class.getSimpleName();
    private static final Executor DIRECT_EXECUTOR = Runnable::run;
    private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
    private static @Nullable Handler sMainHandler;

    private final @NonNull Object mLock = new Object();
    @GuardedBy("mLock")
    private @Nullable BiConsumer<? super T, ? super Throwable> mListener;
    @GuardedBy("mLock")
    private @Nullable Executor mListenerExecutor = DIRECT_EXECUTOR;
    private @NonNull Handler mTimeoutHandler = getMainHandler();
    private final @Nullable IAndroidFuture mRemoteOrigin;

    public AndroidFuture() {
        super();
        mRemoteOrigin = null;
    }

    AndroidFuture(Parcel in) {
        super();
        if (in.readBoolean()) {
            // Done
            if (in.readBoolean()) {
                // Failed
                completeExceptionally(readThrowable(in));
            } else {
                // Success
                complete((T) in.readValue(null));
            }
            mRemoteOrigin = null;
        } else {
            // Not done
            mRemoteOrigin = IAndroidFuture.Stub.asInterface(in.readStrongBinder());
        }
    }

    @NonNull
    private static Handler getMainHandler() {
        // This isn't thread-safe but we are okay with it.
        if (sMainHandler == null) {
            sMainHandler = new Handler(Looper.getMainLooper());
        }
        return sMainHandler;
    }

    /**
     * Create a completed future with the given value.
     *
     * @param value the value for the completed future
     * @param <U> the type of the value
     * @return the completed future
     */
    @NonNull
    public static <U> AndroidFuture<U> completedFuture(U value) {
        AndroidFuture<U> future = new AndroidFuture<>();
        future.complete(value);
        return future;
    }

    @Override
    public boolean complete(@Nullable T value) {
        boolean changed = super.complete(value);
        if (changed) {
            onCompleted(value, null);
        }
        return changed;
    }

    @Override
    public boolean completeExceptionally(@NonNull Throwable ex) {
        boolean changed = super.completeExceptionally(ex);
        if (changed) {
            onCompleted(null, ex);
        }
        return changed;
    }

    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        boolean changed = super.cancel(mayInterruptIfRunning);
        if (changed) {
            try {
                get();
                throw new IllegalStateException("Expected CancellationException");
            } catch (CancellationException ex) {
                onCompleted(null, ex);
            } catch (Throwable e) {
                throw new IllegalStateException("Expected CancellationException", e);
            }
        }
        return changed;
    }

    @CallSuper
    protected void onCompleted(@Nullable T res, @Nullable Throwable err) {
        cancelTimeout();

        if (DEBUG) {
            Slog.i(LOG_TAG, this + " completed with result " + (err == null ? res : err),
                    new RuntimeException());
        }

        BiConsumer<? super T, ? super Throwable> listener;
        synchronized (mLock) {
            listener = mListener;
            mListener = null;
        }

        if (listener != null) {
            callListenerAsync(listener, res, err);
        }

        if (mRemoteOrigin != null) {
            try {
                mRemoteOrigin.complete(this /* resultContainer */);
            } catch (RemoteException e) {
                Slog.e(LOG_TAG, "Failed to propagate completion", e);
            }
        }
    }

    @Override
    public AndroidFuture<T> whenComplete(@NonNull BiConsumer<? super T, ? super Throwable> action) {
        return whenCompleteAsync(action, DIRECT_EXECUTOR);
    }

    @Override
    public AndroidFuture<T> whenCompleteAsync(
            @NonNull BiConsumer<? super T, ? super Throwable> action,
            @NonNull Executor executor) {
        Preconditions.checkNotNull(action);
        Preconditions.checkNotNull(executor);
        synchronized (mLock) {
            if (!isDone()) {
                BiConsumer<? super T, ? super Throwable> oldListener = mListener;

                if (oldListener != null && executor != mListenerExecutor) {
                    // 2 listeners with different executors
                    // Too complex - give up on saving allocations and delegate to superclass
                    super.whenCompleteAsync(action, executor);
                    return this;
                }

                mListenerExecutor = executor;
                mListener = oldListener == null
                        ? action
                        : (res, err) -> {
                            callListener(oldListener, res, err);
                            callListener(action, res, err);
                        };
                return this;
            }
        }

        // isDone() == true at this point
        T res = null;
        Throwable err = null;
        try {
            res = get();
        } catch (ExecutionException e) {
            err = e.getCause();
        } catch (Throwable e) {
            err = e;
        }
        callListenerAsync(action, res, err);
        return this;
    }

    private void callListenerAsync(BiConsumer<? super T, ? super Throwable> listener,
            @Nullable T res, @Nullable Throwable err) {
        synchronized (mLock) {
            if (mListenerExecutor == DIRECT_EXECUTOR) {
                callListener(listener, res, err);
            } else {
                mListenerExecutor.execute(() -> callListener(listener, res, err));
            }
        }
    }

    /**
     * Calls the provided listener, handling any exceptions that may arise.
     */
    // package-private to avoid synthetic method when called from lambda
    static <TT> void callListener(
            @NonNull BiConsumer<? super TT, ? super Throwable> listener,
            @Nullable TT res, @Nullable Throwable err) {
        try {
            try {
                listener.accept(res, err);
            } catch (Throwable t) {
                if (err == null) {
                    // listener happy-case threw, but exception case might not throw, so report the
                    // same exception thrown by listener's happy-path to it again
                    listener.accept(null, t);
                } else {
                    // listener exception-case threw
                    // give up on listener but preserve the original exception when throwing up
                    t.addSuppressed(err);
                    throw t;
                }
            }
        } catch (Throwable t2) {
            // give up on listener and log the result & exception to logcat
            Slog.e(LOG_TAG, "Failed to call whenComplete listener. res = " + res, t2);
        }
    }

    /** @inheritDoc */
    //@Override //TODO uncomment once java 9 APIs are exposed to frameworks
    public AndroidFuture<T> orTimeout(long timeout, @NonNull TimeUnit unit) {
        mTimeoutHandler.postDelayed(this::triggerTimeout, this, unit.toMillis(timeout));
        return this;
    }

    void triggerTimeout() {
        cancelTimeout();
        if (!isDone()) {
            completeExceptionally(new TimeoutException());
        }
    }

    /**
     * Cancel all timeouts previously set with {@link #orTimeout}, if any.
     *
     * @return {@code this} for chaining
     */
    public AndroidFuture<T> cancelTimeout() {
        mTimeoutHandler.removeCallbacksAndMessages(this);
        return this;
    }

    /**
     * Specifies the handler on which timeout is to be triggered
     */
    public AndroidFuture<T> setTimeoutHandler(@NonNull Handler h) {
        cancelTimeout();
        mTimeoutHandler = Preconditions.checkNotNull(h);
        return this;
    }

    @Override
    public <U> AndroidFuture<U> thenCompose(
            @NonNull Function<? super T, ? extends CompletionStage<U>> fn) {
        return thenComposeAsync(fn, DIRECT_EXECUTOR);
    }

    @Override
    public <U> AndroidFuture<U> thenComposeAsync(
            @NonNull Function<? super T, ? extends CompletionStage<U>> fn,
            @NonNull Executor executor) {
        return new ThenComposeAsync<>(this, fn, executor);
    }

    private static class ThenComposeAsync<T, U> extends AndroidFuture<U>
            implements BiConsumer<Object, Throwable>, Runnable {
        private volatile T mSourceResult = null;
        private final Executor mExecutor;
        private volatile Function<? super T, ? extends CompletionStage<U>> mFn;

        ThenComposeAsync(@NonNull AndroidFuture<T> source,
                @NonNull Function<? super T, ? extends CompletionStage<U>> fn,
                @NonNull Executor executor) {
            mFn = Preconditions.checkNotNull(fn);
            mExecutor = Preconditions.checkNotNull(executor);

            // subscribe to first job completion
            source.whenComplete(this);
        }

        @Override
        public void accept(Object res, Throwable err) {
            if (err != null) {
                // first or second job failed
                completeExceptionally(err);
            } else if (mFn != null) {
                // first job completed
                mSourceResult = (T) res;
                // subscribe to second job completion asynchronously
                mExecutor.execute(this);
            } else {
                // second job completed
                complete((U) res);
            }
        }

        @Override
        public void run() {
            CompletionStage<U> secondJob;
            try {
                secondJob = Preconditions.checkNotNull(mFn.apply(mSourceResult));
            } catch (Throwable t) {
                completeExceptionally(t);
                return;
            } finally {
                // Marks first job complete
                mFn = null;
            }
            // subscribe to second job completion
            secondJob.whenComplete(this);
        }
    }

    @Override
    public <U> AndroidFuture<U> thenApply(@NonNull Function<? super T, ? extends U> fn) {
        return thenApplyAsync(fn, DIRECT_EXECUTOR);
    }

    @Override
    public <U> AndroidFuture<U> thenApplyAsync(@NonNull Function<? super T, ? extends U> fn,
            @NonNull Executor executor) {
        return new ThenApplyAsync<>(this, fn, executor);
    }

    private static class ThenApplyAsync<T, U> extends AndroidFuture<U>
            implements BiConsumer<T, Throwable>, Runnable {
        private volatile T mSourceResult = null;
        private final Executor mExecutor;
        private final Function<? super T, ? extends U> mFn;

        ThenApplyAsync(@NonNull AndroidFuture<T> source,
                @NonNull Function<? super T, ? extends U> fn,
                @NonNull Executor executor) {
            mExecutor = Preconditions.checkNotNull(executor);
            mFn = Preconditions.checkNotNull(fn);

            // subscribe to job completion
            source.whenComplete(this);
        }

        @Override
        public void accept(T res, Throwable err) {
            if (err != null) {
                completeExceptionally(err);
            } else {
                mSourceResult = res;
                mExecutor.execute(this);
            }
        }

        @Override
        public void run() {
            try {
                complete(mFn.apply(mSourceResult));
            } catch (Throwable t) {
                completeExceptionally(t);
            }
        }
    }

    @Override
    public <U, V> AndroidFuture<V> thenCombine(
            @NonNull CompletionStage<? extends U> other,
            @NonNull BiFunction<? super T, ? super U, ? extends V> combineResults) {
        return new ThenCombine<T, U, V>(this, other, combineResults);
    }

    /** @see CompletionStage#thenCombine */
    public AndroidFuture<T> thenCombine(@NonNull CompletionStage<Void> other) {
        return thenCombine(other, (res, aVoid) -> res);
    }

    private static class ThenCombine<T, U, V> extends AndroidFuture<V>
            implements BiConsumer<Object, Throwable> {
        private volatile @Nullable T mResultT = null;
        private volatile @NonNull CompletionStage<? extends U> mSourceU;
        private final @NonNull BiFunction<? super T, ? super U, ? extends V> mCombineResults;

        ThenCombine(CompletableFuture<T> sourceT,
                CompletionStage<? extends U> sourceU,
                BiFunction<? super T, ? super U, ? extends V> combineResults) {
            mSourceU = Preconditions.checkNotNull(sourceU);
            mCombineResults = Preconditions.checkNotNull(combineResults);

            sourceT.whenComplete(this);
        }

        @Override
        public void accept(Object res, Throwable err) {
            if (err != null) {
                completeExceptionally(err);
                return;
            }

            if (mSourceU != null) {
                // T done
                mResultT = (T) res;

                // Subscribe to the second job completion.
                mSourceU.whenComplete((r, e) -> {
                    // Mark the first job completion by setting mSourceU to null, so that next time
                    // the execution flow goes to the else case below.
                    mSourceU = null;
                    accept(r, e);
                });
            } else {
                // U done
                try {
                    complete(mCombineResults.apply(mResultT, (U) res));
                } catch (Throwable t) {
                    completeExceptionally(t);
                }
            }
        }
    }

    /**
     * Similar to {@link CompletableFuture#supplyAsync} but
     * runs the given action directly.
     *
     * The resulting future is immediately completed.
     */
    public static <T> AndroidFuture<T> supply(Supplier<T> supplier) {
        return supplyAsync(supplier, DIRECT_EXECUTOR);
    }

    /**
     * @see CompletableFuture#supplyAsync(Supplier, Executor)
     */
    public static <T> AndroidFuture<T> supplyAsync(Supplier<T> supplier, Executor executor) {
        return new SupplyAsync<>(supplier, executor);
    }

    private static class SupplyAsync<T> extends AndroidFuture<T> implements Runnable {
        private final @NonNull Supplier<T> mSupplier;

        SupplyAsync(Supplier<T> supplier, Executor executor) {
            mSupplier = supplier;
            executor.execute(this);
        }

        @Override
        public void run() {
            try {
                complete(mSupplier.get());
            } catch (Throwable t) {
                completeExceptionally(t);
            }
        }
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        boolean done = isDone();
        dest.writeBoolean(done);
        if (done) {
            T result;
            try {
                result = get();
            } catch (Throwable t) {
                dest.writeBoolean(true);
                writeThrowable(dest, unwrapExecutionException(t));
                return;
            }
            dest.writeBoolean(false);
            dest.writeValue(result);
        } else {
            dest.writeStrongBinder(new IAndroidFuture.Stub() {
                @Override
                public void complete(AndroidFuture resultContainer) {
                    boolean changed;
                    try {
                        changed = AndroidFuture.this.complete((T) resultContainer.get());
                    } catch (Throwable t) {
                        changed = completeExceptionally(unwrapExecutionException(t));
                    }
                    if (!changed) {
                        Slog.w(LOG_TAG, "Remote result " + resultContainer
                                + " ignored, as local future is already completed: "
                                + AndroidFuture.this);
                    }
                }
            }.asBinder());
        }
    }

    /**
     * Exceptions coming out of {@link #get} are wrapped in {@link ExecutionException}
     */
    Throwable unwrapExecutionException(Throwable t) {
        return t instanceof ExecutionException
                ? t.getCause()
                : t;
    }

    /**
     * Alternative to {@link Parcel#writeException} that stores the stack trace, in a
     * way consistent with the binder IPC exception propagation behavior.
     */
    private static void writeThrowable(@NonNull Parcel parcel, @Nullable Throwable throwable) {
        boolean hasThrowable = throwable != null;
        parcel.writeBoolean(hasThrowable);
        if (!hasThrowable) {
            return;
        }

        boolean isFrameworkParcelable = throwable instanceof Parcelable
                && throwable.getClass().getClassLoader() == Parcelable.class.getClassLoader();
        parcel.writeBoolean(isFrameworkParcelable);
        if (isFrameworkParcelable) {
            parcel.writeParcelable((Parcelable) throwable,
                    Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
            return;
        }

        parcel.writeString(throwable.getClass().getName());
        parcel.writeString(throwable.getMessage());
        StackTraceElement[] stackTrace = throwable.getStackTrace();
        StringBuilder stackTraceBuilder = new StringBuilder();
        int truncatedStackTraceLength = Math.min(stackTrace != null ? stackTrace.length : 0, 5);
        for (int i = 0; i < truncatedStackTraceLength; i++) {
            if (i > 0) {
                stackTraceBuilder.append('\n');
            }
            stackTraceBuilder.append("\tat ").append(stackTrace[i]);
        }
        parcel.writeString(stackTraceBuilder.toString());
        writeThrowable(parcel, throwable.getCause());
    }

    /**
     * @see #writeThrowable
     */
    private static @Nullable Throwable readThrowable(@NonNull Parcel parcel) {
        final boolean hasThrowable = parcel.readBoolean();
        if (!hasThrowable) {
            return null;
        }

        boolean isFrameworkParcelable = parcel.readBoolean();
        if (isFrameworkParcelable) {
            return parcel.readParcelable(Parcelable.class.getClassLoader());
        }

        String className = parcel.readString();
        String message = parcel.readString();
        String stackTrace = parcel.readString();
        String messageWithStackTrace = message + '\n' + stackTrace;
        Throwable throwable;
        try {
            Class<?> clazz = Class.forName(className, true, Parcelable.class.getClassLoader());
            if (Throwable.class.isAssignableFrom(clazz)) {
                Constructor<?> constructor = clazz.getConstructor(String.class);
                throwable = (Throwable) constructor.newInstance(messageWithStackTrace);
            } else {
                android.util.EventLog.writeEvent(0x534e4554, "186530450", -1, "");
                throwable = new RuntimeException(className + ": " + messageWithStackTrace);
            }
        } catch (Throwable t) {
            throwable = new RuntimeException(className + ": " + messageWithStackTrace);
            throwable.addSuppressed(t);
        }
        throwable.setStackTrace(EMPTY_STACK_TRACE);
        Throwable cause = readThrowable(parcel);
        if (cause != null) {
            throwable.initCause(cause);
        }
        return throwable;
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
    public int describeContents() {
        return 0;
    }

    public static final @NonNull Parcelable.Creator<AndroidFuture> CREATOR =
            new Parcelable.Creator<AndroidFuture>() {
                public AndroidFuture createFromParcel(Parcel parcel) {
                    return new AndroidFuture(parcel);
                }

                public AndroidFuture[] newArray(int size) {
                    return new AndroidFuture[size];
                }
            };
}
