/**
 * 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.telephony.imsmedia;

import android.hardware.radio.ims.media.IImsMediaSession;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.VisibleForTesting;
import android.telephony.CallQuality;
import android.telephony.ims.RtpHeaderExtension;
import android.telephony.imsmedia.AudioConfig;
import android.telephony.imsmedia.IImsAudioSession;
import android.telephony.imsmedia.IImsAudioSessionCallback;
import android.telephony.imsmedia.ImsMediaSession;
import android.telephony.imsmedia.MediaQualityStatus;
import android.telephony.imsmedia.MediaQualityThreshold;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.telephony.imsmedia.Utils.OpenSessionParams;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Audio session binder implementation which handles all audio session APIs
 * from the VOIP applications.
 */
public final class AudioSession extends IImsAudioSession.Stub implements IMediaSession {
    private static final String TAG = "AudioSession";

    public static final int CMD_OPEN_SESSION = 101;
    public static final int CMD_CLOSE_SESSION = 102;
    public static final int CMD_MODIFY_SESSION = 103;
    public static final int CMD_ADD_CONFIG = 104;
    public static final int CMD_DELETE_CONFIG = 105;
    public static final int CMD_CONFIRM_CONFIG = 106;
    public static final int CMD_SEND_DTMF = 107;
    public static final int CMD_SEND_RTP_HDR_EXTN = 108;
    public static final int CMD_SET_MEDIA_QUALITY_THRESHOLD = 109;
    public static final int CMD_START_DTMF = 110;
    public static final int CMD_STOP_DTMF = 111;

    public static final int EVENT_OPEN_SESSION_SUCCESS = 201;
    public static final int EVENT_OPEN_SESSION_FAILURE = 202;
    public static final int EVENT_MODIFY_SESSION_RESPONSE = 203;
    public static final int EVENT_ADD_CONFIG_RESPONSE = 204;
    public static final int EVENT_CONFIRM_CONFIG_RESPONSE = 205;
    public static final int EVENT_FIRST_MEDIA_PACKET_IND = 206;
    public static final int EVENT_RTP_HEADER_EXTENSION_IND = 207;
    public static final int EVENT_MEDIA_QUALITY_STATUS_IND = 208;
    public static final int EVENT_TRIGGER_ANBR_QUERY_IND = 209;
    public static final int EVENT_DTMF_RECEIVED_IND = 210;
    public static final int EVENT_CALL_QUALITY_CHANGE_IND = 211;
    public static final int EVENT_SESSION_CLOSED = 212;

    private static final int DTMF_DEFAULT_DURATION = 140;

    private int mSessionId;
    private AudioOffloadService mOffloadService;
    private AudioOffloadListener mOffloadListener;
    private IImsAudioSessionCallback mCallback;
    private IImsMediaSession mHalSession;
    private AudioSessionHandler mHandler;
    private boolean mIsAudioOffload;
    private AudioService mAudioService;
    private AudioListener mAudioListener;
    private AudioLocalSession mLocalSession;

    AudioSession(final int sessionId, final IImsAudioSessionCallback callback) {
        mSessionId = sessionId;
        mCallback = callback;
        mHandler = new AudioSessionHandler(Looper.getMainLooper());
        if (isAudioOffload()) {
            Log.d(TAG, "Initialize offload service");
            mOffloadService = AudioOffloadService.getInstance();
            mOffloadListener = new AudioOffloadListener(mHandler);
        } else {
            Log.d(TAG, "Initialize local audio service");
            mAudioService = new AudioService();
            mAudioListener = new AudioListener(mHandler);
            mAudioService.setListener(mAudioListener);
            mAudioListener.setNativeObject(mAudioService.getNativeObject());
        }
    }

    @VisibleForTesting
    AudioSession(final int sessionId,
            @NonNull final IImsAudioSessionCallback callback,
            @Nullable final AudioService audioService,
            @Nullable final AudioLocalSession localSession,
            @Nullable final AudioOffloadService offloadService,
            Looper looper) {
        mSessionId = sessionId;
        mCallback = callback;
        mHandler = new AudioSessionHandler(looper);
        mAudioService = audioService;
        mLocalSession = localSession;
        mAudioListener = new AudioListener(mHandler);
        mOffloadService = offloadService;
        mOffloadListener = new AudioOffloadListener(mHandler);
    }

    @VisibleForTesting
    void setAudioOffload(boolean isOffload) {
        mIsAudioOffload = isOffload;
    }

    @VisibleForTesting
    AudioSessionHandler getAudioSessionHandler() {
        return mHandler;
    }

    @VisibleForTesting
    AudioListener getAudioListener() {
        return mAudioListener;
    }

    AudioOffloadListener getOffloadListener() {
        return mOffloadListener;
    }

    @Override
    public void openSession(OpenSessionParams sessionParams) {
        Utils.sendMessage(mHandler, CMD_OPEN_SESSION, sessionParams);
    }

    @Override
    public void closeSession() {
        Utils.sendMessage(mHandler, CMD_CLOSE_SESSION);
    }

    @Override
    public int getSessionId() {
        return mSessionId;
    }

    @Override
    public void modifySession(AudioConfig config) {
        Log.d(TAG, "modifySession: " + config);
        Utils.sendMessage(mHandler, CMD_MODIFY_SESSION, config);
    }

    @Override
    public void addConfig(AudioConfig config) {
        Log.d(TAG, "addConfig: " + config);
        Utils.sendMessage(mHandler, CMD_ADD_CONFIG, config);
    }

    @Override
    public void deleteConfig(AudioConfig config) {
        Log.d(TAG, "deleteConfig: " + config);
        Utils.sendMessage(mHandler, CMD_DELETE_CONFIG, config);
    }

    @Override
    public void confirmConfig(AudioConfig config) {
        Log.d(TAG, "confirmConfig: " + config);
        Utils.sendMessage(mHandler, CMD_CONFIRM_CONFIG, config);
    }

    @Override
    public void sendDtmf(char digit, int duration) {
        Log.d(TAG, "sendDtmf: digit=" + digit + ",duration=" + duration);
        Utils.sendMessage(mHandler, CMD_SEND_DTMF, duration, Utils.UNUSED, digit);
    }

    @Override
    public void startDtmf(char digit) {
        Log.d(TAG, "startDtmf: digit=" + digit);
        Utils.sendMessage(mHandler, CMD_START_DTMF, digit);
    }

    @Override
    public void stopDtmf() {
        Log.d(TAG, "stopDtmf");
        Utils.sendMessage(mHandler, CMD_STOP_DTMF);
    }
    @Override
    public void sendHeaderExtension(List<RtpHeaderExtension> extensions) {
        Log.d(TAG, "sendHeaderExtension");
        Utils.sendMessage(mHandler, CMD_SEND_RTP_HDR_EXTN, extensions);
    }

    @Override
    public void setMediaQualityThreshold(MediaQualityThreshold threshold) {
        Log.d(TAG, "setMediaQualityThreshold: " + threshold);
        Utils.sendMessage(mHandler, CMD_SET_MEDIA_QUALITY_THRESHOLD, threshold);
    }

    @Override
    public void onOpenSessionSuccess(Object session) {
        Log.d(TAG, "onOpenSessionSuccess");
        Utils.sendMessage(mHandler, EVENT_OPEN_SESSION_SUCCESS, session);
    }

    @Override
    public void onOpenSessionFailure(int error) {
        Log.d(TAG, "onOpenSessionFailure: error=" + error);
        Utils.sendMessage(mHandler, EVENT_OPEN_SESSION_FAILURE, error);
    }

    @Override
    public void onSessionClosed() {
        Log.d(TAG, "onSessionClosed");
        Utils.sendMessage(mHandler, EVENT_SESSION_CLOSED);
    }

    private boolean isAudioOffload() {
        return mIsAudioOffload;
    }

    /**
     * Audio session message mHandler
     */
    class AudioSessionHandler extends Handler {
        AudioSessionHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage (Message msg) {
            Log.d(TAG, "handleMessage() -" + AudioSessionHandler.this + ", " + msg.what);
            switch(msg.what) {
                case CMD_OPEN_SESSION:
                    handleOpenSession((OpenSessionParams)msg.obj);
                    break;
                case CMD_CLOSE_SESSION:
                    handleCloseSession();
                    break;
                case CMD_MODIFY_SESSION:
                    handleModifySession((AudioConfig)msg.obj);
                    break;
                case CMD_ADD_CONFIG:
                    handleAddConfig((AudioConfig)msg.obj);
                    break;
                case CMD_DELETE_CONFIG:
                    handleDeleteConfig((AudioConfig)msg.obj);
                    break;
                case CMD_CONFIRM_CONFIG:
                    handleConfirmConfig((AudioConfig)msg.obj);
                    break;
                case CMD_SEND_DTMF:
                    handleSendDtmf((char) msg.obj, msg.arg1);
                    break;
                case CMD_START_DTMF:
                    handleStartDtmf((char) msg.obj);
                    break;
                case CMD_STOP_DTMF:
                    handleStopDtmf();
                    break;
                case CMD_SEND_RTP_HDR_EXTN:
                    handleSendRtpHeaderExtension((List<RtpHeaderExtension>)msg.obj);
                    break;
                case CMD_SET_MEDIA_QUALITY_THRESHOLD:
                    handleSetMediaQualityThreshold((MediaQualityThreshold)msg.obj);
                    break;
                case EVENT_OPEN_SESSION_SUCCESS:
                    handleOpenSuccess(msg.obj);
                    break;
                case EVENT_OPEN_SESSION_FAILURE:
                    handleOpenFailure((int)msg.obj);
                    break;
                case EVENT_SESSION_CLOSED:
                    handleSessionClosed();
                    break;
                case EVENT_MODIFY_SESSION_RESPONSE:
                    handleModifySessionRespose((AudioConfig)msg.obj, msg.arg1);
                    break;
                case EVENT_ADD_CONFIG_RESPONSE:
                    handleAddConfigResponse((AudioConfig)msg.obj, msg.arg1);
                    break;
                case EVENT_CONFIRM_CONFIG_RESPONSE:
                    handleConfirmConfigResponse((AudioConfig)msg.obj, msg.arg1);
                    break;
                case EVENT_FIRST_MEDIA_PACKET_IND:
                    handleFirstMediaPacketInd((AudioConfig)msg.obj);
                    break;
                case EVENT_RTP_HEADER_EXTENSION_IND:
                    handleRtpHeaderExtensionInd((List<RtpHeaderExtension>)msg.obj);
                    break;
                case EVENT_MEDIA_QUALITY_STATUS_IND:
                    handleNotifyMediaQualityStatus((MediaQualityStatus) msg.obj);
                    break;
                case EVENT_TRIGGER_ANBR_QUERY_IND:
                    handleTriggerAnbrQuery((AudioConfig) msg.obj);
                    break;
                case EVENT_DTMF_RECEIVED_IND:
                    handleDtmfReceived((char) msg.arg1, msg.arg2);
                    break;
                case EVENT_CALL_QUALITY_CHANGE_IND:
                    handleCallQualityChangeInd((CallQuality) msg.obj);
                    break;
                default:
            }
        }
    }

    private void handleOpenSession(OpenSessionParams sessionParams) {
        if (isAudioOffload()) {
            mOffloadService.openSession(mSessionId, sessionParams);
        } else {
            mAudioListener.setMediaCallback(sessionParams.getCallback());
            int result = mAudioService.openSession(mSessionId, sessionParams);
            if (result != ImsMediaSession.RESULT_SUCCESS) {
                handleOpenFailure(result);
            }
        }
    }

    private void handleCloseSession() {
        Log.d(TAG, "handleCloseSession");
        if (isAudioOffload()) {
            mOffloadService.closeSession(mSessionId);
        } else {
            mAudioService.closeSession(mSessionId);
        }
    }

    private void handleModifySession(AudioConfig config) {
        if (isAudioOffload()) {
            try {
                mHalSession.modifySession(Utils.convertToRtpConfig(config));
            } catch (RemoteException e) {
                Log.e(TAG, "modifySession : " + e);
            }
        } else {
            mLocalSession.modifySession(config);
        }
    }

    private void handleAddConfig(AudioConfig config) {
        if (isAudioOffload()) {
            try {
                mHalSession.modifySession(Utils.convertToRtpConfig(config));
            } catch (RemoteException e) {
                Log.e(TAG, "addConfig : " + e);
            }
        } else {
            mLocalSession.addConfig(config);
        }
    }

    private void handleDeleteConfig(AudioConfig config) {
        if (!isAudioOffload()) {
            mLocalSession.deleteConfig(config);
        }
    }

    private void handleConfirmConfig(AudioConfig config) {
        if (!isAudioOffload()) {
            mLocalSession.confirmConfig(config);
        }
    }

    private void handleSendDtmf(char digit, int duration) {
        if (isAudioOffload()) {
            try {
                mHalSession.sendDtmf(digit, duration);
            } catch (RemoteException e) {
                Log.e(TAG, "sendDtmf : " + e);
            }
        } else {
            mLocalSession.sendDtmf(digit, duration);
        }
    }

    private void handleStartDtmf(char digit) {
        if (isAudioOffload()) {
            try {
                mHalSession.startDtmf(digit);
            } catch (RemoteException e) {
                Log.e(TAG, "startDtmf : " + e);
            }
        } else {
            mLocalSession.sendDtmf(digit, DTMF_DEFAULT_DURATION);
        }
    }

    private void handleStopDtmf() {
        if (isAudioOffload()) {
            try {
                mHalSession.stopDtmf();
            } catch (RemoteException e) {
                Log.e(TAG, "stopDtmf : " + e);
            }
        }
    }

    private void handleSendRtpHeaderExtension(List<RtpHeaderExtension> extensions) {
        if (isAudioOffload()) {
            try {
                List<android.hardware.radio.ims.media.RtpHeaderExtension>
                        halExtensions = extensions.stream().map(Utils::convertRtpHeaderExtension)
                                .collect(Collectors.toList());
                mHalSession.sendHeaderExtension(halExtensions);
            } catch (RemoteException e) {
                Log.e(TAG, "sendHeaderExtension : " + e);
            }
        } else {
            mLocalSession.sendHeaderExtension(extensions);
        }
    }

    private void handleSetMediaQualityThreshold(MediaQualityThreshold threshold) {
        if (isAudioOffload()) {
            try {
                mHalSession.setMediaQualityThreshold(Utils.convertMediaQualityThreshold(threshold));
            } catch (RemoteException e) {
                Log.e(TAG, "setMediaQualityThreshold: " + e);
            }
        } else {
            mLocalSession.setMediaQualityThreshold(threshold);
        }
    }

    private void handleOpenSuccess(Object session) {
       if (session instanceof IImsMediaSession) {
            try {
                ((IImsMediaSession)session).setListener(mOffloadListener);
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to notify openSuccess: " + e);
            }
            mHalSession = (IImsMediaSession) session;
        } else {
            mLocalSession = (AudioLocalSession)session;
        }

        try {
            mCallback.onOpenSessionSuccess(this);
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to notify openSuccess: " + e);
        }
    }

    private void handleOpenFailure(int error) {
        try {
            mCallback.onOpenSessionFailure(error);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify openFailure: " + e);
        }
    }

    private void handleSessionClosed() {
        try {
            mCallback.onSessionClosed();
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify SessionClosed: " + e);
        }
    }

    private void handleModifySessionRespose(AudioConfig config, int error) {
        try {
            mCallback.onModifySessionResponse(config, error);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify modifySessionResponse: " + e);
        }
    }

    private void handleAddConfigResponse(AudioConfig config, int error) {
        try {
            mCallback.onAddConfigResponse(config, error);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify onAddConfigResponse: " + e);
        }
    }

    private void handleConfirmConfigResponse(AudioConfig config, int error) {
        try {
            mCallback.onConfirmConfigResponse(config, error);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify onConfirmConfigResponse: " + e);
        }
    }

    private void handleFirstMediaPacketInd(AudioConfig config) {
        try {
            mCallback.onFirstMediaPacketReceived(config);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify first media packet received indication: " + e);
        }
    }

    private void handleRtpHeaderExtensionInd(List<RtpHeaderExtension> extensions) {
        try {
            mCallback.onHeaderExtensionReceived(extensions);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify RTP header extension: " + e);
        }
    }

    private void handleNotifyMediaQualityStatus(MediaQualityStatus status) {
        try {
            mCallback.notifyMediaQualityStatus(status);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify media quality status: " + e);
        }

    }

    private void handleTriggerAnbrQuery(AudioConfig config) {
        try {
            mCallback.triggerAnbrQuery(config);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to trigger ANBR query: " + e);
        }
    }

    private void handleDtmfReceived(char dtmfDigit, int durationMs) {
        try {
            mCallback.onDtmfReceived(dtmfDigit, durationMs);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to Dtmf received: " + e);
        }
    }

    private void handleCallQualityChangeInd(CallQuality callQuality) {
        try {
            mCallback.onCallQualityChanged(callQuality);
        }  catch (RemoteException e) {
            Log.e(TAG, "Failed to notify call quality changed indication: " + e);
        }
    }
}
