/*
 * Copyright 2019 HIMSA II K/S - www.himsa.com.
 * Represented by EHIMA - www.ehima.com
 *
 * 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;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.content.AttributionSource;
import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;

/**
 * This class provides the APIs to control the Call Control profile.
 *
 * <p>
 * This class provides Bluetooth Telephone Bearer Service functionality,
 * allowing applications to expose a GATT Service based interface to control the
 * state of the calls by remote devices such as LE audio devices.
 *
 * <p>
 * BluetoothLeCallControl is a proxy object for controlling the Bluetooth Telephone Bearer
 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the
 * BluetoothLeCallControl proxy object.
 *
 * @hide
 */
public final class BluetoothLeCallControl implements BluetoothProfile {
    private static final String TAG = "BluetoothLeCallControl";
    private static final boolean DBG = true;
    private static final boolean VDBG = false;

    /** @hide */
    @IntDef(prefix = "RESULT_", value = {
            RESULT_SUCCESS,
            RESULT_ERROR_UNKNOWN_CALL_ID,
            RESULT_ERROR_INVALID_URI,
            RESULT_ERROR_APPLICATION
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Result {
    }

    /**
     * Opcode write was successful.
     *
     * @hide
     */
    public static final int RESULT_SUCCESS = 0;

    /**
     * Unknown call Id has been used in the operation.
     *
     * @hide
     */
    public static final int RESULT_ERROR_UNKNOWN_CALL_ID = 1;

    /**
     * The URI provided in {@link Callback#onPlaceCallRequest} is invalid.
     *
     * @hide
     */
    public static final int RESULT_ERROR_INVALID_URI = 2;

    /**
     * Application internal error.
     *
     * @hide
     */
    public static final int RESULT_ERROR_APPLICATION = 3;

    /** @hide */
    @IntDef(prefix = "TERMINATION_REASON_", value = {
            TERMINATION_REASON_INVALID_URI,
            TERMINATION_REASON_FAIL,
            TERMINATION_REASON_REMOTE_HANGUP,
            TERMINATION_REASON_SERVER_HANGUP,
            TERMINATION_REASON_LINE_BUSY,
            TERMINATION_REASON_NETWORK_CONGESTION,
            TERMINATION_REASON_CLIENT_HANGUP,
            TERMINATION_REASON_NO_SERVICE,
            TERMINATION_REASON_NO_ANSWER
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface TerminationReason {
    }

    /**
     * Remote Caller ID value used to place a call was formed improperly.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_INVALID_URI = 0x00;

    /**
     * Call fail.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_FAIL = 0x01;

    /**
     * Remote party ended call.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_REMOTE_HANGUP = 0x02;

    /**
     * Call ended from the server.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_SERVER_HANGUP = 0x03;

    /**
     * Line busy.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_LINE_BUSY = 0x04;

    /**
     * Network congestion.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_NETWORK_CONGESTION = 0x05;

    /**
     * Client terminated.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_CLIENT_HANGUP = 0x06;

    /**
     * No service.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_NO_SERVICE = 0x07;

    /**
     * No answer.
     *
     * @hide
     */
    public static final int TERMINATION_REASON_NO_ANSWER = 0x08;

    /*
     * Flag indicating support for hold/unhold call feature.
     *
     * @hide
     */
    public static final int CAPABILITY_HOLD_CALL = 0x00000001;

    /**
     * Flag indicating support for joining calls feature.
     *
     * @hide
     */
    public static final int CAPABILITY_JOIN_CALLS = 0x00000002;

    private static final int REG_TIMEOUT = 10000;

    /**
     * The template class is used to call callback functions on events from the TBS
     * server. Callback functions are wrapped in this class and registered to the
     * Android system during app registration.
     *
     * @hide
     */
    public abstract static class Callback {

        private static final String TAG = "BluetoothLeCallControl.Callback";

        /**
         * Called when a remote client requested to accept the call.
         *
         * <p>
         * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
         * request.
         *
         * @param requestId The Id of the request
         * @param callId    The call Id requested to be accepted
         * @hide
         */
        public abstract void onAcceptCall(int requestId, @NonNull UUID callId);

        /**
         * A remote client has requested to terminate the call.
         *
         * <p>
         * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
         * request.
         *
         * @param requestId The Id of the request
         * @param callId    The call Id requested to terminate
         * @hide
         */
        public abstract void onTerminateCall(int requestId, @NonNull UUID callId);

        /**
         * A remote client has requested to hold the call.
         *
         * <p>
         * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
         * request.
         *
         * @param requestId The Id of the request
         * @param callId    The call Id requested to be put on hold
         * @hide
         */
        public void onHoldCall(int requestId, @NonNull UUID callId) {
            Log.e(TAG, "onHoldCall: unimplemented, however CAPABILITY_HOLD_CALL is set!");
        }

        /**
         * A remote client has requested to unhold the call.
         *
         * <p>
         * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
         * request.
         *
         * @param requestId The Id of the request
         * @param callId    The call Id requested to unhold
         * @hide
         */
        public void onUnholdCall(int requestId, @NonNull UUID callId) {
            Log.e(TAG, "onUnholdCall: unimplemented, however CAPABILITY_HOLD_CALL is set!");
        }

        /**
         * A remote client has requested to place a call.
         *
         * <p>
         * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
         * request.
         *
         * @param requestId The Id of the request
         * @param callId    The Id to be assigned for the new call
         * @param uri       The caller URI requested
         * @hide
         */
        public abstract void onPlaceCall(int requestId, @NonNull UUID callId, @NonNull String uri);

        /**
         * A remote client has requested to join the calls.
         *
         * <p>
         * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
         * request.
         *
         * @param requestId The Id of the request
         * @param callIds   The call Id list requested to join
         * @hide
         */
        public void onJoinCalls(int requestId, @NonNull List<UUID> callIds) {
            Log.e(TAG, "onJoinCalls: unimplemented, however CAPABILITY_JOIN_CALLS is set!");
        }
    }

    private class CallbackWrapper extends IBluetoothLeCallControlCallback.Stub {

        private final Executor mExecutor;
        private final Callback mCallback;

        CallbackWrapper(Executor executor, Callback callback) {
            mExecutor = executor;
            mCallback = callback;
        }

        @Override
        public void onBearerRegistered(int ccid) {
            if (mCallback != null) {
                mCcid = ccid;
            } else {
                // registration timeout
                Log.e(TAG, "onBearerRegistered: mCallback is null");
            }
        }

        @Override
        public void onAcceptCall(int requestId, ParcelUuid uuid) {
            final long identityToken = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mCallback.onAcceptCall(requestId, uuid.getUuid()));
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
        }

        @Override
        public void onTerminateCall(int requestId, ParcelUuid uuid) {
            final long identityToken = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mCallback.onTerminateCall(requestId, uuid.getUuid()));
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
        }

        @Override
        public void onHoldCall(int requestId, ParcelUuid uuid) {
            final long identityToken = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mCallback.onHoldCall(requestId, uuid.getUuid()));
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
        }

        @Override
        public void onUnholdCall(int requestId, ParcelUuid uuid) {
            final long identityToken = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mCallback.onUnholdCall(requestId, uuid.getUuid()));
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
        }

        @Override
        public void onPlaceCall(int requestId, ParcelUuid uuid, String uri) {
            final long identityToken = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mCallback.onPlaceCall(requestId, uuid.getUuid(), uri));
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
        }

        @Override
        public void onJoinCalls(int requestId, List<ParcelUuid> parcelUuids) {
            List<UUID> uuids = new ArrayList<>();
            for (ParcelUuid parcelUuid : parcelUuids) {
                uuids.add(parcelUuid.getUuid());
            }

            final long identityToken = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mCallback.onJoinCalls(requestId, uuids));
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
        }
    };

    private BluetoothAdapter mAdapter;
    private final AttributionSource mAttributionSource;
    private int mCcid = 0;
    private String mToken;
    private Callback mCallback = null;
    private final BluetoothProfileConnector<IBluetoothLeCallControl> mProfileConnector =
            new BluetoothProfileConnector(this, BluetoothProfile.LE_CALL_CONTROL,
                    "BluetoothLeCallControl", IBluetoothLeCallControl.class.getName()) {
                @Override
                public IBluetoothLeCallControl getServiceInterface(IBinder service) {
                    return IBluetoothLeCallControl.Stub.asInterface(service);
                }
    };


    /**
     * Create a BluetoothLeCallControl proxy object for interacting with the local Bluetooth
     * telephone bearer service.
     */
    /* package */ BluetoothLeCallControl(Context context, ServiceListener listener) {
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mAttributionSource = mAdapter.getAttributionSource();
        mProfileConnector.connect(context, listener);
    }

    /** @hide */
    @Override
    public void close() {
        if (VDBG) log("close()");

        unregisterBearer();

        mProfileConnector.disconnect();
    }

    private IBluetoothLeCallControl getService() {
        return mProfileConnector.getService();
    }

    /**
     * Not supported
     *
     * @throws UnsupportedOperationException
     */
    @Override
    public int getConnectionState(@Nullable BluetoothDevice device) {
        throw new UnsupportedOperationException("not supported");
    }

    /**
     * Not supported
     *
     * @throws UnsupportedOperationException
     */
    @Override
    public @NonNull List<BluetoothDevice> getConnectedDevices() {
        throw new UnsupportedOperationException("not supported");
    }

    /**
     * Not supported
     *
     * @throws UnsupportedOperationException
     */
    @Override
    @NonNull
    public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
        throw new UnsupportedOperationException("not supported");
    }

    /**
     * Register Telephone Bearer exposing the interface that allows remote devices
     * to track and control the call states.
     *
     * <p>
     * This is an asynchronous call. The callback is used to notify success or
     * failure if the function returns true.
     *
     * <p>
     * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * <!-- The UCI is a String identifier of the telephone bearer as defined at
     * https://www.bluetooth.com/specifications/assigned-numbers/uniform-caller-identifiers
     * (login required). -->
     *
     * <!-- The examples of common URI schemes can be found in
     * https://iana.org/assignments/uri-schemes/uri-schemes.xhtml -->
     *
     * <!-- The Technology is an integer value. The possible values are defined at
     * https://www.bluetooth.com/specifications/assigned-numbers (login required).
     * -->
     *
     * @param uci          Bearer Unique Client Identifier
     * @param uriSchemes   URI Schemes supported list
     * @param capabilities bearer capabilities
     * @param provider     Network provider name
     * @param technology   Network technology
     * @param executor     {@link Executor} object on which callback will be
     *                     executed. The Executor object is required.
     * @param callback     {@link Callback} object to which callback messages will
     *                     be sent. The Callback object is required.
     * @return true on success, false otherwise
     * @hide
     */
    @SuppressLint("ExecutorRegistration")
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public boolean registerBearer(@Nullable String uci,
                    @NonNull List<String> uriSchemes, int capabilities,
                    @NonNull String provider, int technology,
                    @NonNull Executor executor, @NonNull Callback callback) {
        if (DBG) {
            Log.d(TAG, "registerBearer");
        }
        if (callback == null) {
            throw new IllegalArgumentException("null parameter: " + callback);
        }
        if (mCcid != 0) {
            return false;
        }

        mToken = uci;

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return false;
        }

        if (mCallback != null) {
            Log.e(TAG, "Bearer can be opened only once");
            return false;
        }

        mCallback = callback;
        try {
            CallbackWrapper callbackWrapper = new CallbackWrapper(executor, callback);
            service.registerBearer(mToken, callbackWrapper, uci, uriSchemes, capabilities,
                                    provider, technology, mAttributionSource);

        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
            mCallback = null;
            return false;
        }

        if (mCcid == 0) {
            mCallback = null;
            return false;
        }

        return true;
    }

    /**
     * Unregister Telephone Bearer Service and destroy all the associated data.
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public void unregisterBearer() {
        if (DBG) {
            Log.d(TAG, "unregisterBearer");
        }
        if (mCcid == 0) {
            return;
        }

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }

        int ccid = mCcid;
        mCcid = 0;
        mCallback = null;

        try {
            service.unregisterBearer(mToken, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }
    }

    /**
     * Get the Content Control ID (CCID) value.
     *
     * @return ccid Content Control ID value
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public int getContentControlId() {
        return mCcid;
    }

    /**
     * Notify about the newly added call.
     *
     * <p>
     * This shall be called as early as possible after the call has been added.
     *
     * <p>
     * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * @param call Newly added call
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public void onCallAdded(@NonNull BluetoothLeCall call) {
        if (DBG) {
            Log.d(TAG, "onCallAdded: call=" + call);
        }
        if (mCcid == 0) {
            return;
        }

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }

        try {
            service.callAdded(mCcid, call, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }
    }

    /**
     * Notify about the removed call.
     *
     * <p>
     * This shall be called as early as possible after the call has been removed.
     *
     * <p>
     * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * @param callId The Id of a call that has been removed
     * @param reason Call termination reason
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public void onCallRemoved(@NonNull UUID callId, @TerminationReason int reason) {
        if (DBG) {
            Log.d(TAG, "callRemoved: callId=" + callId);
        }
        if (mCcid == 0) {
            return;
        }

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }
        try {
            service.callRemoved(mCcid, new ParcelUuid(callId), reason, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }

    }

    /**
     * Notify the call state change
     *
     * <p>
     * This shall be called as early as possible after the state of the call has
     * changed.
     *
     * <p>
     * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * @param callId The call Id that state has been changed
     * @param state  Call state
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public void onCallStateChanged(@NonNull UUID callId, @BluetoothLeCall.State int state) {
        if (DBG) {
            Log.d(TAG, "callStateChanged: callId=" + callId + " state=" + state);
        }
        if (mCcid == 0) {
            return;
        }

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }

        try {
            service.callStateChanged(mCcid, new ParcelUuid(callId), state, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }
    }

    /**
     * Provide the current calls list
     *
     * <p>
     * This function must be invoked after registration if application has any
     * calls.
     *
     * @param calls current calls list
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public void currentCallsList(@NonNull List<BluetoothLeCall> calls) {
        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }

        try {
            service.currentCallsList(mCcid, calls, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }

    }

    /**
     * Provide the network current status
     *
     * <p>
     * This function must be invoked on change of network state.
     *
     * <p>
     * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * <!-- The Technology is an integer value. The possible values are defined at
     * https://www.bluetooth.com/specifications/assigned-numbers (login required).
     * -->
     *
     * @param provider   Network provider name
     * @param technology Network technology
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public void networkStateChanged(@NonNull String provider, int technology) {
        if (DBG) {
            Log.d(TAG, "networkStateChanged: provider=" + provider + ", technology=" + technology);
        }
        if (mCcid == 0) {
            return;
        }

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }

        try {
            service.networkStateChanged(mCcid, provider, technology, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }
    }

    /**
     * Send a response to a call control request to a remote device.
     *
     * <p>
     * This function must be invoked in when a request is received by one of these
     * callback methods:
     *
     * <ul>
     * <li>{@link Callback#onAcceptCall}
     * <li>{@link Callback#onTerminateCall}
     * <li>{@link Callback#onHoldCall}
     * <li>{@link Callback#onUnholdCall}
     * <li>{@link Callback#onPlaceCall}
     * <li>{@link Callback#onJoinCalls}
     * </ul>
     *
     * @param requestId The ID of the request that was received with the callback
     * @param result    The result of the request to be sent to the remote devices
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    public void requestResult(int requestId, @Result int result) {
        if (DBG) {
            Log.d(TAG, "requestResult: requestId=" + requestId + " result=" + result);
        }
        if (mCcid == 0) {
            return;
        }

        final IBluetoothLeCallControl service = getService();
        if (service == null) {
            Log.w(TAG, "Proxy not attached to service");
            return;
        }

        try {
            service.requestResult(mCcid, requestId, result, mAttributionSource);
        } catch (RemoteException e) {
            Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
        }
    }

    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    private static boolean isValidDevice(@Nullable BluetoothDevice device) {
        return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
    }

    private static void log(String msg) {
        Log.d(TAG, msg);
    }
}
