/*
 * Copyright (C) 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 com.android.internal.telecom;

import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;

import android.os.Binder;
import android.os.Bundle;
import android.os.OutcomeReceiver;
import android.os.ResultReceiver;
import android.telecom.CallAttributes;
import android.telecom.CallControl;
import android.telecom.CallControlCallback;
import android.telecom.CallEndpoint;
import android.telecom.CallEventCallback;
import android.telecom.CallException;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
import android.util.Log;

import com.android.server.telecom.flags.Flags;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * wraps {@link CallControlCallback}, {@link CallEventCallback}, and {@link CallControl} on a
 * per-{@link  android.telecom.PhoneAccountHandle} basis to track ongoing calls.
 *
 * @hide
 */
public class ClientTransactionalServiceWrapper {

    private static final String TAG = ClientTransactionalServiceWrapper.class.getSimpleName();
    private final PhoneAccountHandle mPhoneAccountHandle;
    private final ClientTransactionalServiceRepository mRepository;
    private final ConcurrentHashMap<String, TransactionalCall> mCallIdToTransactionalCall =
            new ConcurrentHashMap<>();
    private static final String EXECUTOR_FAIL_MSG =
            "Telecom hit an exception while handling a CallEventCallback on an executor: ";

    public ClientTransactionalServiceWrapper(PhoneAccountHandle handle,
            ClientTransactionalServiceRepository repo) {
        mPhoneAccountHandle = handle;
        mRepository = repo;
    }

    /**
     * remove the given call from the class HashMap
     *
     * @param callId that is tied to TransactionalCall object
     */
    public void untrackCall(String callId) {
        Log.i(TAG, TextUtils.formatSimple("removeCall: with id=[%s]", callId));
        if (mCallIdToTransactionalCall.containsKey(callId)) {
            // remove the call from the hashmap
            TransactionalCall call = mCallIdToTransactionalCall.remove(callId);
            // null out interface to avoid memory leaks
            CallControl control = call.getCallControl();
            if (control != null) {
                call.setCallControl(null);
            }
        }
        // possibly cleanup service wrapper if there are no more calls
        if (mCallIdToTransactionalCall.size() == 0) {
            mRepository.removeServiceWrapper(mPhoneAccountHandle);
        }
    }

    /**
     * start tracking a newly created call for a particular package
     *
     * @param callAttributes of the new call
     * @param executor       to run callbacks on
     * @param pendingControl that allows telecom to call into the client
     * @param handshakes     that overrides the CallControlCallback
     * @param events         that overrides the CallStateCallback
     * @return the callId of the newly created call
     */
    public String trackCall(CallAttributes callAttributes, Executor executor,
            OutcomeReceiver<CallControl, CallException> pendingControl,
            CallControlCallback handshakes,
            CallEventCallback events) {
        // generate a new id for this new call
        String newCallId = UUID.randomUUID().toString();

        // couple the objects passed from the client side
        mCallIdToTransactionalCall.put(newCallId, new TransactionalCall(newCallId, callAttributes,
                executor, pendingControl, handshakes, events));

        return newCallId;
    }

    public ICallEventCallback getCallEventCallback() {
        return mCallEventCallback;
    }

    /**
     * Consumers that is to be completed by the client and the result relayed back to telecom server
     * side via a {@link ResultReceiver}. see com.android.server.telecom.TransactionalServiceWrapper
     * for how the response is handled.
     */
    private class ReceiverWrapper implements Consumer<Boolean> {
        private final ResultReceiver mRepeaterReceiver;

        ReceiverWrapper(ResultReceiver resultReceiver) {
            mRepeaterReceiver = resultReceiver;
        }

        @Override
        public void accept(Boolean clientCompletedCallbackSuccessfully) {
            if (clientCompletedCallbackSuccessfully) {
                mRepeaterReceiver.send(TELECOM_TRANSACTION_SUCCESS, null);
            } else {
                mRepeaterReceiver.send(CallException.CODE_ERROR_UNKNOWN, null);
            }
        }

        @Override
        public Consumer<Boolean> andThen(Consumer<? super Boolean> after) {
            return Consumer.super.andThen(after);
        }
    }

    private final ICallEventCallback mCallEventCallback = new ICallEventCallback.Stub() {

        private static final String ON_SET_ACTIVE = "onSetActive";
        private static final String ON_SET_INACTIVE = "onSetInactive";
        private static final String ON_ANSWER = "onAnswer";
        private static final String ON_DISCONNECT = "onDisconnect";
        private static final String ON_STREAMING_STARTED = "onStreamingStarted";
        private static final String ON_REQ_ENDPOINT_CHANGE = "onRequestEndpointChange";
        private static final String ON_AVAILABLE_CALL_ENDPOINTS = "onAvailableCallEndpointsChanged";
        private static final String ON_MUTE_STATE_CHANGED = "onMuteStateChanged";
        private static final String ON_VIDEO_STATE_CHANGED = "onVideoStateChanged";
        private static final String ON_CALL_STREAMING_FAILED = "onCallStreamingFailed";
        private static final String ON_EVENT = "onEvent";

        private void handleCallEventCallback(String action, String callId,
                ResultReceiver ackResultReceiver, Object... args) {
            Log.i(TAG, TextUtils.formatSimple("hCEC: id=[%s], action=[%s]", callId, action));
            // lookup the callEventCallback associated with the particular call
            TransactionalCall call = mCallIdToTransactionalCall.get(callId);

            if (call != null) {
                // Get the CallEventCallback interface
                CallControlCallback callback = call.getCallControlCallback();
                // Get Receiver to wait on client ack
                ReceiverWrapper outcomeReceiverWrapper = new ReceiverWrapper(ackResultReceiver);

                // wait for the client to complete the CallEventCallback
                final long identity = Binder.clearCallingIdentity();
                try {
                    call.getExecutor().execute(() -> {
                        switch (action) {
                            case ON_SET_ACTIVE:
                                callback.onSetActive(outcomeReceiverWrapper);
                                break;
                            case ON_SET_INACTIVE:
                                callback.onSetInactive(outcomeReceiverWrapper);
                                break;
                            case ON_DISCONNECT:
                                callback.onDisconnect((DisconnectCause) args[0],
                                        outcomeReceiverWrapper);
                                untrackCall(callId);
                                break;
                            case ON_ANSWER:
                                callback.onAnswer((int) args[0], outcomeReceiverWrapper);
                                break;
                            case ON_STREAMING_STARTED:
                                callback.onCallStreamingStarted(outcomeReceiverWrapper);
                                break;
                        }
                    });
                } catch (Exception e) {
                    Log.e(TAG, EXECUTOR_FAIL_MSG + e);
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }

        @Override
        public void onAddCallControl(String callId, int resultCode, ICallControl callControl,
                CallException transactionalException) {
            Log.i(TAG, TextUtils.formatSimple("oACC: id=[%s], code=[%d]", callId, resultCode));
            TransactionalCall call = mCallIdToTransactionalCall.get(callId);

            if (call != null) {
                OutcomeReceiver<CallControl, CallException> pendingControl =
                        call.getPendingControl();

                if (resultCode == TELECOM_TRANSACTION_SUCCESS) {

                    // create the interface object that the client will interact with
                    CallControl control = new CallControl(callId, callControl);
                    // give the client the object via the OR that was passed into addCall
                    pendingControl.onResult(control);

                    // store for later reference
                    call.setCallControl(control);
                } else {
                    pendingControl.onError(transactionalException);
                    mCallIdToTransactionalCall.remove(callId);
                }

            } else {
                untrackCall(callId);
                Log.e(TAG, "oACC: TransactionalCall object not found for call w/ id=" + callId);
            }
        }

        @Override
        public void onSetActive(String callId, ResultReceiver resultReceiver) {
            handleCallEventCallback(ON_SET_ACTIVE, callId, resultReceiver);
        }

        @Override
        public void onSetInactive(String callId, ResultReceiver resultReceiver) {
            handleCallEventCallback(ON_SET_INACTIVE, callId, resultReceiver);
        }

        @Override
        public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) {
            handleCallEventCallback(ON_ANSWER, callId, resultReceiver, videoState);
        }

        @Override
        public void onDisconnect(String callId, DisconnectCause cause,
                ResultReceiver resultReceiver) {
            handleCallEventCallback(ON_DISCONNECT, callId, resultReceiver, cause);
        }

        @Override
        public void onCallEndpointChanged(String callId, CallEndpoint endpoint) {
            handleEventCallback(callId, ON_REQ_ENDPOINT_CHANGE, endpoint);
        }

        @Override
        public void onAvailableCallEndpointsChanged(String callId, List<CallEndpoint> endpoints) {
            handleEventCallback(callId, ON_AVAILABLE_CALL_ENDPOINTS, endpoints);
        }

        @Override
        public void onMuteStateChanged(String callId, boolean isMuted) {
            handleEventCallback(callId, ON_MUTE_STATE_CHANGED, isMuted);
        }

        @Override
        public void onVideoStateChanged(String callId, int videoState) {
            handleEventCallback(callId, ON_VIDEO_STATE_CHANGED, videoState);
        }

        public void handleEventCallback(String callId, String action, Object arg) {
            Log.d(TAG, TextUtils.formatSimple("hEC: [%s], callId=[%s]", action, callId));
            // lookup the callEventCallback associated with the particular call
            TransactionalCall call = mCallIdToTransactionalCall.get(callId);
            if (call != null) {
                CallEventCallback callback = call.getCallStateCallback();
                Executor executor = call.getExecutor();
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> {
                        switch (action) {
                            case ON_REQ_ENDPOINT_CHANGE:
                                callback.onCallEndpointChanged((CallEndpoint) arg);
                                break;
                            case ON_AVAILABLE_CALL_ENDPOINTS:
                                callback.onAvailableCallEndpointsChanged((List<CallEndpoint>) arg);
                                break;
                            case ON_MUTE_STATE_CHANGED:
                                callback.onMuteStateChanged((boolean) arg);
                                break;
                            case ON_VIDEO_STATE_CHANGED:
                                if (Flags.transactionalVideoState()) {
                                    callback.onVideoStateChanged((int) arg);
                                }
                                break;
                            case ON_CALL_STREAMING_FAILED:
                                callback.onCallStreamingFailed((int) arg /* reason */);
                                break;
                        }
                    });
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }

        @Override
        public void removeCallFromTransactionalServiceWrapper(String callId) {
            untrackCall(callId);
        }

        @Override
        public void onCallStreamingStarted(String callId, ResultReceiver resultReceiver) {
            handleCallEventCallback(ON_STREAMING_STARTED, callId, resultReceiver);
        }

        @Override
        public void onCallStreamingFailed(String callId, int reason) {
            Log.i(TAG, TextUtils.formatSimple("oCSF: id=[%s], reason=[%s]", callId, reason));
            handleEventCallback(callId, ON_CALL_STREAMING_FAILED, reason);
        }

        @Override
        public void onEvent(String callId, String event, Bundle extras) {
            // lookup the callEventCallback associated with the particular call
            TransactionalCall call = mCallIdToTransactionalCall.get(callId);
            if (call != null) {
                CallEventCallback callback = call.getCallStateCallback();
                Executor executor = call.getExecutor();
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> {
                        callback.onEvent(event, extras);
                    });
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }
    };
}
