/*
 * 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.server.telecom;

import static android.telecom.CallStreamingService.STREAMING_FAILED_SENDER_BINDING_ERROR;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.IBinder;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.CallException;
import android.telecom.CallStreamingService;
import android.telecom.StreamingCall;
import android.telecom.Log;

import com.android.internal.telecom.ICallStreamingService;
import com.android.server.telecom.voip.ParallelTransaction;
import com.android.server.telecom.voip.SerialTransaction;
import com.android.server.telecom.voip.VoipCallTransaction;
import com.android.server.telecom.voip.VoipCallTransactionResult;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

public class CallStreamingController extends CallsManagerListenerBase {
    private Call mStreamingCall;
    private TransactionalServiceWrapper mTransactionalServiceWrapper;
    private ICallStreamingService mService;
    private final Context mContext;
    private CallStreamingServiceConnection mConnection;
    private boolean mIsStreaming;
    private final Object mLock;
    private TelecomSystem.SyncRoot mTelecomLock;

    public CallStreamingController(Context context, TelecomSystem.SyncRoot telecomLock) {
        mLock = new Object();
        mContext = context;
        mTelecomLock = telecomLock;
    }

    private void onConnectedInternal(Call call, TransactionalServiceWrapper wrapper,
            IBinder service) throws RemoteException {
        synchronized (mLock) {
            Log.i(this, "onConnectedInternal: callid=%s", call.getId());
            Bundle extras = new Bundle();
            extras.putString(StreamingCall.EXTRA_CALL_ID, call.getId());
            mStreamingCall = call;
            mTransactionalServiceWrapper = wrapper;
            mService = ICallStreamingService.Stub.asInterface(service);
            mService.setStreamingCallAdapter(new StreamingCallAdapter(mTransactionalServiceWrapper,
                    mStreamingCall,
                    mStreamingCall.getTargetPhoneAccount().getComponentName().getPackageName()));
            mService.onCallStreamingStarted(new StreamingCall(
                    mTransactionalServiceWrapper.getComponentName(),
                    mStreamingCall.getCallerDisplayName(),
                    mStreamingCall.getHandle(), extras));
            mIsStreaming = true;
        }
    }

    private void resetController() {
        synchronized (mLock) {
            mStreamingCall = null;
            mTransactionalServiceWrapper = null;
            if (mConnection != null) {
                // Notify service streaming stopped and then unbind.
                try {
                    mService.onCallStreamingStopped();
                } catch (RemoteException e) {
                    // Could not notify stop streaming; we're about to just unbind so this is
                    // unfortunate but not the end of the world.
                    Log.e(this, e, "resetController: failed to notify stop streaming.");
                }
                mContext.unbindService(mConnection);
                mConnection = null;
            }
            mService = null;
            mIsStreaming = false;
        }
    }

    public boolean isStreaming() {
        synchronized (mLock) {
            return mIsStreaming;
        }
    }

    public static class QueryCallStreamingTransaction extends VoipCallTransaction {
        private final CallsManager mCallsManager;

        public QueryCallStreamingTransaction(CallsManager callsManager) {
            super(callsManager.getLock());
            mCallsManager = callsManager;
        }

        @Override
        public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
            Log.i(this, "processTransaction");
            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();

            if (mCallsManager.getCallStreamingController().isStreaming()) {
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        "STREAMING_FAILED_ALREADY_STREAMING"));
            } else {
                future.complete(new VoipCallTransactionResult(
                        VoipCallTransactionResult.RESULT_SUCCEED, null));
            }

            return future;
        }
    }

    public static class AudioInterceptionTransaction extends VoipCallTransaction {
        private Call mCall;
        private boolean mEnterInterception;

        public AudioInterceptionTransaction(Call call, boolean enterInterception,
                TelecomSystem.SyncRoot lock) {
            super(lock);
            mCall = call;
            mEnterInterception = enterInterception;
        }

        @Override
        public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
            Log.i(this, "processTransaction");
            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();

            if (mEnterInterception) {
                mCall.startStreaming();
            } else {
                mCall.stopStreaming();
            }
            future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
                    null));
            return future;
        }
    }

    public StreamingServiceTransaction getCallStreamingServiceTransaction(Context context,
            TransactionalServiceWrapper wrapper, Call call) {
        return new StreamingServiceTransaction(context, wrapper, call);
    }

    public class StreamingServiceTransaction extends VoipCallTransaction {
        public static final String MESSAGE = "STREAMING_FAILED_NO_SENDER";
        private final TransactionalServiceWrapper mWrapper;
        private final Context mContext;
        private final UserHandle mUserHandle;
        private final Call mCall;

        public StreamingServiceTransaction(Context context, TransactionalServiceWrapper wrapper,
                Call call) {
            super(mTelecomLock);
            mWrapper = wrapper;
            mCall = call;
            mUserHandle = mCall.getAssociatedUser();
            mContext = context;
        }

        @SuppressLint("LongLogTag")
        @Override
        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
            Log.i(this, "processTransaction");
            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
            RoleManager roleManager = mContext.getSystemService(RoleManager.class);
            PackageManager packageManager = mContext.getPackageManager();
            if (roleManager == null || packageManager == null) {
                Log.w(this, "processTransaction: Can't find system service");
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        MESSAGE));
                return future;
            }

            List<String> holders = roleManager.getRoleHoldersAsUser(
                    RoleManager.ROLE_SYSTEM_CALL_STREAMING, mUserHandle);
            if (holders.isEmpty()) {
                Log.w(this, "processTransaction: Can't find streaming app");
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        MESSAGE));
                return future;
            }
            Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
            Intent serviceIntent = new Intent(CallStreamingService.SERVICE_INTERFACE);
            serviceIntent.setPackage(holders.get(0));
            List<ResolveInfo> infos = packageManager.queryIntentServicesAsUser(serviceIntent,
                    PackageManager.GET_META_DATA, mUserHandle);
            if (infos.isEmpty()) {
                Log.w(this, "processTransaction: Can't find streaming service");
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        MESSAGE));
                return future;
            }

            ServiceInfo serviceInfo = infos.get(0).serviceInfo;

            if (serviceInfo.permission == null || !serviceInfo.permission.equals(
                    Manifest.permission.BIND_CALL_STREAMING_SERVICE)) {
                Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
                        serviceInfo.packageName);
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        MESSAGE));
                return future;
            }
            Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
            intent.setComponent(serviceInfo.getComponentName());

            mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
            if (!mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE
                    | Context.BIND_FOREGROUND_SERVICE
                    | Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
                Log.w(this, "Can't bind to streaming service");
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        "STREAMING_FAILED_SENDER_BINDING_ERROR"));
            }
            return future;
        }
    }

    public UnbindStreamingServiceTransaction getUnbindStreamingServiceTransaction() {
        return new UnbindStreamingServiceTransaction();
    }

    public class UnbindStreamingServiceTransaction extends VoipCallTransaction {
        public UnbindStreamingServiceTransaction() {
            super(mTelecomLock);
        }

        @SuppressLint("LongLogTag")
        @Override
        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
            Log.i(this, "processTransaction (unbindStreaming");
            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();

            resetController();
            future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
                    null));
            return future;
        }
    }

    public class StartStreamingTransaction extends SerialTransaction {
        private Call mCall;

        public StartStreamingTransaction(List<VoipCallTransaction> subTransactions, Call call,
                TelecomSystem.SyncRoot lock) {
            super(subTransactions, lock);
            mCall = call;
        }

        @Override
        public void handleTransactionFailure() {
            mTransactionalServiceWrapper.stopCallStreaming(mCall);
        }
    }

    public VoipCallTransaction getStartStreamingTransaction(CallsManager callsManager,
            TransactionalServiceWrapper wrapper, Call call, TelecomSystem.SyncRoot lock) {
        // start streaming transaction flow:
        //     1. make sure there's no ongoing streaming call --> bind to EXO
        //     2. change audio mode
        //     3. bind to EXO
        // If bind to EXO failed, add transaction for stop the streaming

        // create list for multiple transactions
        List<VoipCallTransaction> transactions = new ArrayList<>();
        transactions.add(new QueryCallStreamingTransaction(callsManager));
        transactions.add(new AudioInterceptionTransaction(call, true, lock));
        transactions.add(getCallStreamingServiceTransaction(
                callsManager.getContext(), wrapper, call));
        return new StartStreamingTransaction(transactions, call, lock);
    }

    public VoipCallTransaction getStopStreamingTransaction(Call call, TelecomSystem.SyncRoot lock) {
        // TODO: implement this
        // Stop streaming transaction flow:
        List<VoipCallTransaction> transactions = new ArrayList<>();

        // 1. unbind to call streaming service
        transactions.add(getUnbindStreamingServiceTransaction());
        // 2. audio route operations
        transactions.add(new CallStreamingController.AudioInterceptionTransaction(call,
                false, lock));
        return new ParallelTransaction(transactions, lock);
    }

    @Override
    public void onCallRemoved(Call call) {
        if (mStreamingCall == call) {
            mTransactionalServiceWrapper.stopCallStreaming(call);
        }
    }

    @Override
    public void onCallStateChanged(Call call, int oldState, int newState) {
        // TODO: make sure we are only able to stream the one call and not switch focus to another
        // and have it streamed too
        if (mStreamingCall == call && oldState != newState) {
            CallStreamingStateChangeTransaction transaction = null;
            switch (newState) {
                case CallState.ACTIVE:
                    transaction = new CallStreamingStateChangeTransaction(
                            StreamingCall.STATE_STREAMING);
                    break;
                case CallState.ON_HOLD:
                    transaction = new CallStreamingStateChangeTransaction(
                            StreamingCall.STATE_HOLDING);
                    break;
                case CallState.DISCONNECTING:
                case CallState.DISCONNECTED:
                    Log.addEvent(call, LogUtils.Events.STOP_STREAMING);
                    transaction = new CallStreamingStateChangeTransaction(
                            StreamingCall.STATE_DISCONNECTED);
                    break;
                default:
                    // ignore
            }
            if (transaction != null) {
                mTransactionalServiceWrapper.getTransactionManager().addTransaction(transaction,
                        new OutcomeReceiver<>() {
                            @Override
                            public void onResult(VoipCallTransactionResult result) {
                                // ignore
                            }

                            @Override
                            public void onError(CallException exception) {
                                Log.e(this, exception, "Exception when set call "
                                        + "streaming state to streaming app");
                            }
                        });
            }
        }
    }

    private class CallStreamingStateChangeTransaction extends VoipCallTransaction {
        @StreamingCall.StreamingCallState int mState;

        public CallStreamingStateChangeTransaction(@StreamingCall.StreamingCallState int state) {
            super(mTelecomLock);
            mState = state;
        }

        @Override
        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
            try {
                mService.onCallStreamingStateChanged(mState);
                future.complete(new VoipCallTransactionResult(
                        VoipCallTransactionResult.RESULT_SUCCEED, null));
            } catch (RemoteException e) {
                future.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        "Exception when request "
                        + "setting state to streaming app."));
            }
            return future;
        }
    }

    private class CallStreamingServiceConnection implements
            ServiceConnection {
        private Call mCall;
        private TransactionalServiceWrapper mWrapper;
        private CompletableFuture<VoipCallTransactionResult> mFuture;

        public CallStreamingServiceConnection(Call call, TransactionalServiceWrapper wrapper,
                CompletableFuture<VoipCallTransactionResult> future) {
            mCall = call;
            mWrapper = wrapper;
            mFuture = future;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            try {
                Log.i(this, "onServiceConnected: " + name);
                onConnectedInternal(mCall, mWrapper, service);
                mFuture.complete(new VoipCallTransactionResult(
                        VoipCallTransactionResult.RESULT_SUCCEED, null));
            } catch (RemoteException e) {
                resetController();
                mFuture.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        StreamingServiceTransaction.MESSAGE));
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            clearBinding();
        }

        @Override
        public void onBindingDied(ComponentName name) {
            clearBinding();
        }

        @Override
        public void onNullBinding(ComponentName name) {
            clearBinding();
        }

        private void clearBinding() {
            resetController();
            if (!mFuture.isDone()) {
                mFuture.complete(new VoipCallTransactionResult(
                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                        "STREAMING_FAILED_SENDER_BINDING_ERROR"));
            } else {
                mWrapper.onCallStreamingFailed(mCall, STREAMING_FAILED_SENDER_BINDING_ERROR);
            }
        }
    }
}
