/*
 * Copyright 2017 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.hardware.location;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Handler;
import android.os.HandlerExecutor;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * A class describing a request sent to the Context Hub Service.
 *
 * This object is generated as a result of an asynchronous request sent to the Context Hub
 * through the ContextHubManager APIs. The caller can either retrieve the result
 * synchronously through a blocking call ({@link #waitForResponse(long, TimeUnit)}) or
 * asynchronously through a user-defined listener
 * ({@link #setOnCompleteListener(OnCompleteListener, Executor)} )}).
 *
 * @param <T> the type of the contents in the transaction response
 *
 * @hide
 */
@SystemApi
public class ContextHubTransaction<T> {
    private static final String TAG = "ContextHubTransaction";

    /**
     * Constants describing the type of a transaction through the Context Hub Service.
     * {@hide}
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = { "TYPE_" }, value = {
            TYPE_LOAD_NANOAPP,
            TYPE_UNLOAD_NANOAPP,
            TYPE_ENABLE_NANOAPP,
            TYPE_DISABLE_NANOAPP,
            TYPE_QUERY_NANOAPPS
    })
    public @interface Type { }

    public static final int TYPE_LOAD_NANOAPP = 0;
    public static final int TYPE_UNLOAD_NANOAPP = 1;
    public static final int TYPE_ENABLE_NANOAPP = 2;
    public static final int TYPE_DISABLE_NANOAPP = 3;
    public static final int TYPE_QUERY_NANOAPPS = 4;

    /**
     * Constants describing the result of a transaction or request through the Context Hub Service.
     * {@hide}
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = { "RESULT_" }, value = {
            RESULT_SUCCESS,
            RESULT_FAILED_UNKNOWN,
            RESULT_FAILED_BAD_PARAMS,
            RESULT_FAILED_UNINITIALIZED,
            RESULT_FAILED_BUSY,
            RESULT_FAILED_AT_HUB,
            RESULT_FAILED_TIMEOUT,
            RESULT_FAILED_SERVICE_INTERNAL_FAILURE,
            RESULT_FAILED_HAL_UNAVAILABLE
    })
    public @interface Result {}
    public static final int RESULT_SUCCESS = 0;
    /**
     * Generic failure mode.
     */
    public static final int RESULT_FAILED_UNKNOWN = 1;
    /**
     * Failure mode when the request parameters were not valid.
     */
    public static final int RESULT_FAILED_BAD_PARAMS = 2;
    /**
     * Failure mode when the Context Hub is not initialized.
     */
    public static final int RESULT_FAILED_UNINITIALIZED = 3;
    /**
     * Failure mode when there are too many transactions pending.
     */
    public static final int RESULT_FAILED_BUSY = 4;
    /**
     * Failure mode when the request went through, but failed asynchronously at the hub.
     */
    public static final int RESULT_FAILED_AT_HUB = 5;
    /**
     * Failure mode when the transaction has timed out.
     */
    public static final int RESULT_FAILED_TIMEOUT = 6;
    /**
     * Failure mode when the transaction has failed internally at the service.
     */
    public static final int RESULT_FAILED_SERVICE_INTERNAL_FAILURE = 7;
    /**
     * Failure mode when the Context Hub HAL was not available.
     */
    public static final int RESULT_FAILED_HAL_UNAVAILABLE = 8;

    /**
     * A class describing the response for a ContextHubTransaction.
     *
     * @param <R> the type of the contents in the response
     */
    public static class Response<R> {
        /*
         * The result of the transaction.
         */
        @ContextHubTransaction.Result
        private int mResult;

        /*
         * The contents of the response from the Context Hub.
         */
        private R mContents;

        Response(@ContextHubTransaction.Result int result, R contents) {
            mResult = result;
            mContents = contents;
        }

        @ContextHubTransaction.Result
        public int getResult() {
            return mResult;
        }

        public R getContents() {
            return mContents;
        }
    }

    /**
     * An interface describing the listener for a transaction completion.
     *
     * @param <L> the type of the contents in the transaction response
     */
    @FunctionalInterface
    public interface OnCompleteListener<L> {
        /**
         * The listener function to invoke when the transaction completes.
         *
         * @param transaction the transaction that this callback was attached to.
         * @param response the response of the transaction.
         */
        void onComplete(
                ContextHubTransaction<L> transaction, ContextHubTransaction.Response<L> response);
    }

    /*
     * The type of the transaction.
     */
    @Type
    private int mTransactionType;

    /*
     * The response of the transaction.
     */
    private ContextHubTransaction.Response<T> mResponse;

    /*
     * The executor to invoke the onComplete async callback.
     */
    private Executor mExecutor = null;

    /*
     * The listener to be invoked when the transaction completes.
     */
    private ContextHubTransaction.OnCompleteListener<T> mListener = null;

    /*
     * Synchronization latch used to block on response.
     */
    private final CountDownLatch mDoneSignal = new CountDownLatch(1);

    /*
     * true if the response has been set throught setResponse, false otherwise.
     */
    private boolean mIsResponseSet = false;

    ContextHubTransaction(@Type int type) {
        mTransactionType = type;
    }

    /**
     * Converts a transaction type to a human-readable string
     *
     * @param type the type of a transaction
     * @param upperCase {@code true} if upper case the first letter, {@code false} otherwise
     * @return a string describing the transaction
     */
    public static String typeToString(@Type int type, boolean upperCase) {
        switch (type) {
            case ContextHubTransaction.TYPE_LOAD_NANOAPP:
                return upperCase ? "Load" : "load";
            case ContextHubTransaction.TYPE_UNLOAD_NANOAPP:
                return upperCase ? "Unload" : "unload";
            case ContextHubTransaction.TYPE_ENABLE_NANOAPP:
                return upperCase ? "Enable" : "enable";
            case ContextHubTransaction.TYPE_DISABLE_NANOAPP:
                return upperCase ? "Disable" : "disable";
            case ContextHubTransaction.TYPE_QUERY_NANOAPPS:
                return upperCase ? "Query" : "query";
            default:
                return upperCase ? "Unknown" : "unknown";
        }
    }

    /**
     * @return the type of the transaction
     */
    @Type
    public int getType() {
        return mTransactionType;
    }

    /**
     * Waits to receive the asynchronous transaction result.
     *
     * This function blocks until the Context Hub Service has received a response
     * for the transaction represented by this object by the Context Hub, or a
     * specified timeout period has elapsed.
     *
     * If the specified timeout has passed, a TimeoutException will be thrown and the caller may
     * retry the invocation of this method at a later time.
     *
     * @param timeout the timeout duration
     * @param unit the unit of the timeout
     *
     * @return the transaction response
     *
     * @throws InterruptedException if the current thread is interrupted while waiting for response
     * @throws TimeoutException if the timeout period has passed
     */
    public ContextHubTransaction.Response<T> waitForResponse(
            long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
        boolean success = mDoneSignal.await(timeout, unit);

        if (!success) {
            throw new TimeoutException("Timed out while waiting for transaction");
        }

        return mResponse;
    }

    /**
     * Sets the listener to be invoked invoked when the transaction completes.
     *
     * This function provides an asynchronous approach to retrieve the result of the
     * transaction. When the transaction response has been provided by the Context Hub,
     * the given listener will be invoked.
     *
     * If the transaction has already completed at the time of invocation, the listener
     * will be immediately invoked. If the transaction has been invalidated,
     * the listener will never be invoked.
     *
     * A transaction can be invalidated if the process owning the transaction is no longer active
     * and the reference to this object is lost.
     *
     * This method or {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener)} can
     * only be invoked once, or an IllegalStateException will be thrown.
     *
     * @param listener the listener to be invoked upon completion
     * @param executor the executor to invoke the callback
     *
     * @throws IllegalStateException if this method is called multiple times
     * @throws NullPointerException if the callback or handler is null
     */
    public void setOnCompleteListener(
            @NonNull ContextHubTransaction.OnCompleteListener<T> listener,
            @NonNull @CallbackExecutor Executor executor) {
        synchronized (this) {
            Preconditions.checkNotNull(listener, "OnCompleteListener cannot be null");
            Preconditions.checkNotNull(executor, "Executor cannot be null");
            if (mListener != null) {
                throw new IllegalStateException(
                        "Cannot set ContextHubTransaction listener multiple times");
            }

            mListener = listener;
            mExecutor = executor;

            if (mDoneSignal.getCount() == 0) {
                mExecutor.execute(() -> mListener.onComplete(this, mResponse));
            }
        }
    }

    /**
     * Sets the listener to be invoked invoked when the transaction completes.
     *
     * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener,
     * Executor)} with the executor using the main thread's Looper.
     *
     * This method or {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener,
     * Executor)} can only be invoked once, or an IllegalStateException will be thrown.
     *
     * @param listener the listener to be invoked upon completion
     *
     * @throws IllegalStateException if this method is called multiple times
     * @throws NullPointerException if the callback is null
     */
    public void setOnCompleteListener(
            @NonNull ContextHubTransaction.OnCompleteListener<T> listener) {
        setOnCompleteListener(listener, new HandlerExecutor(Handler.getMain()));
    }

    /**
     * Sets the response of the transaction.
     *
     * This method should only be invoked by ContextHubManager as a result of a callback from
     * the Context Hub Service indicating the response from a transaction. This method should not be
     * invoked more than once.
     *
     * @param response the response to set
     *
     * @throws IllegalStateException if this method is invoked multiple times
     * @throws NullPointerException if the response is null
     */
    /* package */ void setResponse(ContextHubTransaction.Response<T> response) {
        synchronized (this) {
            Preconditions.checkNotNull(response, "Response cannot be null");
            if (mIsResponseSet) {
                throw new IllegalStateException(
                        "Cannot set response of ContextHubTransaction multiple times");
            }

            mResponse = response;
            mIsResponseSet = true;

            mDoneSignal.countDown();
            if (mListener != null) {
                mExecutor.execute(() -> mListener.onComplete(this, mResponse));
            }
        }
    }
}
