/*
 * Copyright (C) 2016 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.modules.utils;


import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;

import java.io.Serializable;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Generic interface for receiving a callback result from someone.
 * Allow the server end to synchronously wait on the response from the client.
 * This enables an RPC like system but with the ability to timeout and discard late results.
 *
 * <p>NOTE: Use the static {@link #get} method to retrieve an available instance of this class.
 * If no instances are available, a new one is created.
 */
public final class SynchronousResultReceiver<T> implements Parcelable {
    private static final String TAG = "SynchronousResultReceiver";
    private final boolean mLocal;
    private boolean mIsCompleted;
    private final static Object sLock = new Object();
    private final static int QUEUE_THRESHOLD = 4;

    @GuardedBy("sLock")
    private CompletableFuture<Result<T>> mFuture = new CompletableFuture<>();

    @GuardedBy("sLock")
    private static final ConcurrentLinkedQueue<SynchronousResultReceiver> sAvailableReceivers
            = new ConcurrentLinkedQueue<>();

    public static <T> SynchronousResultReceiver<T> get() {
        synchronized(sLock) {
            if (sAvailableReceivers.isEmpty()) {
                return new SynchronousResultReceiver();
            }
            SynchronousResultReceiver receiver = sAvailableReceivers.poll();
            receiver.resetLocked();
            return receiver;
        }
    }

    private SynchronousResultReceiver() {
        mLocal = true;
        mIsCompleted = false;
    }

    @GuardedBy("sLock")
    private void releaseLocked() {
        mFuture = null;
        if (sAvailableReceivers.size() < QUEUE_THRESHOLD) {
            sAvailableReceivers.add(this);
        }
    }

    @GuardedBy("sLock")
    private void resetLocked() {
        mFuture = new CompletableFuture<>();
        mIsCompleted = false;
    }

    private CompletableFuture<Result<T>> getFuture() {
       synchronized (sLock) {
           return mFuture;
       }
    }

    public static class Result<T> implements Parcelable {
        private final @Nullable T mObject;
        private final RuntimeException mException;

        public Result(RuntimeException exception) {
            mObject = null;
            mException = exception;
        }

        public Result(@Nullable T object) {
            mObject = object;
            mException = null;
        }

        /**
         * Return the stored value
         * May throw a {@link RuntimeException} thrown from the client
         */
        public T getValue(T defaultValue) {
            if (mException != null) {
                throw mException;
            }
            if (mObject == null) {
                return defaultValue;
            }
            return mObject;
        }

        public int describeContents() {
            return 0;
        }

        public void writeToParcel(@NonNull Parcel out, int flags) {
            out.writeValue(mObject);
            out.writeValue(mException);
        }

        private Result(Parcel in) {
            mObject = (T)in.readValue(null);
            mException= (RuntimeException)in.readValue(null);
        }

        public static final @NonNull Parcelable.Creator<Result<?>> CREATOR =
            new Parcelable.Creator<Result<?>>() {
                public Result createFromParcel(Parcel in) {
                    return new Result(in);
                }
                public Result[] newArray(int size) {
                    return new Result[size];
                }
            };
    }

    private void complete(Result<T> result) {
        if (mIsCompleted) {
            throw new IllegalStateException("Receiver has already been completed");
        }
        mIsCompleted = true;
        if (mLocal) {
            getFuture().complete(result);
        } else {
            final ISynchronousResultReceiver rr;
            synchronized (this) {
                rr = mReceiver;
            }
            if (rr != null) {
                try {
                    rr.send(result);
                } catch (RemoteException e) {
                    Log.w(TAG, "Failed to complete future");
                }
            }
        }
    }

    /**
     * Deliver a result to this receiver.
     *
     * @param resultData Additional data provided by you.
     */
    public void send(@Nullable T resultData) {
        complete(new Result<>(resultData));
    }

    /**
     * Deliver an {@link Exception} to this receiver
     *
     * @param e exception to be sent
     */
    public void propagateException(@NonNull RuntimeException e) {
        Objects.requireNonNull(e, "RuntimeException cannot be null");
        complete(new Result<>(e));
    }

    /**
     * Blocks waiting for the result from the remote client.
     *
     * If it is interrupted before completion of the duration, wait again with remaining time until
     * the deadline.
     *
     * @param timeout The duration to wait before sending a {@link TimeoutException}
     * @return the Result
     * @throws TimeoutException if the timeout in milliseconds expired.
     */
    public @NonNull Result<T> awaitResultNoInterrupt(@NonNull Duration timeout)
            throws TimeoutException {
        Objects.requireNonNull(timeout, "Null timeout is not allowed");

        final long startWaitNanoTime = SystemClock.elapsedRealtimeNanos();
        Duration remainingTime = timeout;
        while (!remainingTime.isNegative()) {
            try {
                Result<T> result = getFuture().get(remainingTime.toMillis(), TimeUnit.MILLISECONDS);
                synchronized (sLock) {
                    releaseLocked();
                    return result;
                }
            } catch (ExecutionException e) {
                // This will NEVER happen.
                throw new AssertionError("Error receiving response", e);
            } catch (InterruptedException e) {
                // The thread was interrupted, try and get the value again, this time
                // with the remaining time until the deadline.
                remainingTime = timeout.minus(
                        Duration.ofNanos(SystemClock.elapsedRealtimeNanos() - startWaitNanoTime));
            }
        }
        synchronized (sLock) {
            releaseLocked();
        }
        throw new TimeoutException();
    }

    ISynchronousResultReceiver mReceiver = null;

    private final class MyResultReceiver extends ISynchronousResultReceiver.Stub {
        public void send(@SuppressWarnings("rawtypes") @NonNull Result result) {
            @SuppressWarnings("unchecked") Result<T> res = (Result<T>) result;
            CompletableFuture<Result<T>> future;
            future = getFuture();
            if (future != null) {
                future.complete(res);
            }
        }
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(@NonNull Parcel out, int flags) {
        synchronized (this) {
            if (mReceiver == null) {
                mReceiver = new MyResultReceiver();
            }
            out.writeStrongBinder(mReceiver.asBinder());
        }
    }

    private SynchronousResultReceiver(Parcel in) {
        mLocal = false;
        mIsCompleted = false;
        mReceiver = ISynchronousResultReceiver.Stub.asInterface(in.readStrongBinder());
    }

    public static final @NonNull Parcelable.Creator<SynchronousResultReceiver<?>> CREATOR =
            new Parcelable.Creator<SynchronousResultReceiver<?>>() {
            public SynchronousResultReceiver<?> createFromParcel(Parcel in) {
                return new SynchronousResultReceiver(in);
            }
            public SynchronousResultReceiver<?>[] newArray(int size) {
                return new SynchronousResultReceiver[size];
            }
        };
}