/*
 * Copyright 2022 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.bluetooth.le;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.IBluetoothGatt;
import android.content.AttributionSource;
import android.os.Binder;
import android.os.ParcelUuid;
import android.os.RemoteException;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * This class provides a way to control an active distance measurement session.
 *
 * <p>It also defines the required {@link DistanceMeasurementSession.Callback} that must be
 * implemented in order to be notified of distance measurement results and status events related to
 * the {@link DistanceMeasurementSession}.
 *
 * <p>To get an instance of {@link DistanceMeasurementSession}, first use {@link
 * DistanceMeasurementManager#startMeasurementSession(DistanceMeasurementParams, Executor,
 * DistanceMeasurementSession.Callback)} to request to start a session. Once the session is started,
 * a {@link DistanceMeasurementSession} object is provided through {@link
 * DistanceMeasurementSession.Callback#onStarted(DistanceMeasurementSession)}. If starting a session
 * fails, the failure is reported through {@link
 * DistanceMeasurementSession.Callback#onStartFail(int)} with the failure reason.
 *
 * @hide
 */
@SystemApi
public final class DistanceMeasurementSession {
    private static final String TAG = "DistanceMeasurementSession";

    private final IBluetoothGatt mGatt;
    private final ParcelUuid mUuid;
    private final DistanceMeasurementParams mDistanceMeasurementParams;
    private final Executor mExecutor;
    private final Callback mCallback;
    private final AttributionSource mAttributionSource;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            value = {
                BluetoothStatusCodes.SUCCESS,
                BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
                BluetoothStatusCodes.ERROR_DISTANCE_MEASUREMENT_INTERNAL,
            })
    public @interface StopSessionReturnValues {}

    /** @hide */
    public DistanceMeasurementSession(
            IBluetoothGatt gatt,
            ParcelUuid uuid,
            DistanceMeasurementParams params,
            Executor executor,
            AttributionSource attributionSource,
            Callback callback) {
        Objects.requireNonNull(gatt, "gatt is null");
        Objects.requireNonNull(params, "params is null");
        Objects.requireNonNull(executor, "executor is null");
        Objects.requireNonNull(callback, "callback is null");
        mGatt = gatt;
        mUuid = uuid;
        mDistanceMeasurementParams = params;
        mExecutor = executor;
        mAttributionSource = attributionSource;
        mCallback = callback;
    }

    /**
     * Stops actively ranging, {@link Callback#onStopped} will be invoked if this succeeds.
     *
     * @return whether successfully stop or not
     * @hide
     */
    @SystemApi
    @RequiresPermission(
            allOf = {
                android.Manifest.permission.BLUETOOTH_CONNECT,
                android.Manifest.permission.BLUETOOTH_PRIVILEGED,
            })
    public @StopSessionReturnValues int stopSession() {
        try {
            return mGatt.stopDistanceMeasurement(
                    mUuid,
                    mDistanceMeasurementParams.getDevice(),
                    mDistanceMeasurementParams.getMethodId(),
                    mAttributionSource);
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /** @hide */
    void onStarted() {
        executeCallback(() -> mCallback.onStarted(this));
    }

    /** @hide */
    void onStartFail(int reason) {
        executeCallback(() -> mCallback.onStartFail(reason));
    }

    /** @hide */
    void onStopped(int reason) {
        executeCallback(() -> mCallback.onStopped(this, reason));
    }

    /** @hide */
    void onResult(@NonNull BluetoothDevice device, @NonNull DistanceMeasurementResult result) {
        executeCallback(() -> mCallback.onResult(device, result));
    }

    /** @hide */
    private void executeCallback(@NonNull Runnable runnable) {
        final long identity = Binder.clearCallingIdentity();
        try {
            mExecutor.execute(runnable);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    /**
     * Interface for receiving {@link DistanceMeasurementSession} events.
     *
     * @hide
     */
    @SystemApi
    public interface Callback {
        /** @hide */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(
                value = {
                    BluetoothStatusCodes.ERROR_UNKNOWN,
                    BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
                    BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
                    BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST,
                    BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
                    BluetoothStatusCodes.REASON_REMOTE_REQUEST,
                    BluetoothStatusCodes.ERROR_TIMEOUT,
                    BluetoothStatusCodes.ERROR_NO_LE_CONNECTION,
                    BluetoothStatusCodes.ERROR_BAD_PARAMETERS,
                    BluetoothStatusCodes.ERROR_DISTANCE_MEASUREMENT_INTERNAL,
                })
        @interface Reason {}

        /**
         * Invoked when {@link DistanceMeasurementManager#startMeasurementSession(
         * DistanceMeasurementParams, Executor, DistanceMeasurementSession.Callback)} is successful.
         *
         * @param session the started {@link DistanceMeasurementSession}
         * @hide
         */
        @SystemApi
        void onStarted(@NonNull DistanceMeasurementSession session);

        /**
         * Invoked if {@link DistanceMeasurementManager#startMeasurementSession(
         * DistanceMeasurementParams, Executor, DistanceMeasurementSession.Callback)} fails.
         *
         * @param reason the failure reason
         * @hide
         */
        @SystemApi
        void onStartFail(@NonNull @Reason int reason);

        /**
         * Invoked when a distance measurement session stopped.
         *
         * @param reason reason for the session stop
         * @hide
         */
        @SystemApi
        void onStopped(@NonNull DistanceMeasurementSession session, @NonNull @Reason int reason);

        /**
         * Invoked when get distance measurement result.
         *
         * @param device remote device
         * @param result {@link DistanceMeasurementResult} for this device
         * @hide
         */
        @SystemApi
        void onResult(@NonNull BluetoothDevice device, @NonNull DistanceMeasurementResult result);
    }
}
