/*
 * Copyright (C) 2017 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.ui;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.ArraySet;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.HandoverState;
import com.android.server.telecom.R;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.components.TelecomBroadcastReceiver;

import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Manages the display of an incoming call UX when a new ringing self-managed call is added, and
 * there is an ongoing call in another {@link android.telecom.PhoneAccount}.
 */
public class IncomingCallNotifier extends CallsManagerListenerBase {

    public interface IncomingCallNotifierFactory {
        IncomingCallNotifier make(Context context, CallsManagerProxy mCallsManagerProxy);
    }

    /**
     * Eliminates strict dependency between this class and CallsManager.
     */
    public interface CallsManagerProxy {
        boolean hasUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle);
        int getNumUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle);
        Call getActiveCall();
    }

    // Notification for incoming calls. This is interruptive and will show up as a HUN.
    @VisibleForTesting
    public static final int NOTIFICATION_INCOMING_CALL = 1;
    @VisibleForTesting
    public static final String NOTIFICATION_TAG = IncomingCallNotifier.class.getSimpleName();
    private final Object mLock = new Object();

    public final Call.ListenerBase mCallListener = new Call.ListenerBase() {
        @Override
        public void onCallerInfoChanged(Call call) {
            if (mIncomingCall != call) {
                return;
            }
            showIncomingCallNotification(mIncomingCall);
        }
    };

    private final Context mContext;
    private final NotificationManager mNotificationManager;
    private final Set<Call> mCalls = new ArraySet<>();
    private CallsManagerProxy mCallsManagerProxy;

    // The current incoming call we are displaying UX for.
    private Call mIncomingCall;

    public IncomingCallNotifier(Context context) {
        mContext = context;
        mNotificationManager =
                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
    }

    public void setCallsManagerProxy(CallsManagerProxy callsManagerProxy) {
        mCallsManagerProxy = callsManagerProxy;
    }

    public Call getIncomingCall() {
        return mIncomingCall;
    }

    @Override
    public void onCallAdded(Call call) {
        synchronized (mLock) {
            if (!mCalls.contains(call)) {
                mCalls.add(call);
            }
        }

        updateIncomingCall();
    }

    @Override
    public void onCallRemoved(Call call) {
        synchronized (mLock) {
            if (mCalls.contains(call)) {
                mCalls.remove(call);
            }
        }
        updateIncomingCall();
    }

    @Override
    public void onCallStateChanged(Call call, int oldState, int newState) {
        updateIncomingCall();
    }

    /**
     * Determines which call is the active ringing call at this time and triggers the display of the
     * UI.
     */
    private void updateIncomingCall() {
        Optional<Call> incomingCallOp;
        synchronized (mLock) {
            incomingCallOp = mCalls.stream()
                    .filter(Objects::nonNull)
                    .filter(call -> call.isSelfManaged() && call.isIncoming() &&
                            call.getState() == CallState.RINGING &&
                            call.getHandoverState() == HandoverState.HANDOVER_NONE)
                    .findFirst();
        }

        Call incomingCall = incomingCallOp.orElse(null);
        if (incomingCall != null && mCallsManagerProxy != null &&
                !mCallsManagerProxy.hasUnholdableCallsForOtherConnectionService(
                        incomingCallOp.get().getTargetPhoneAccount())) {
            // If there is no calls in any other ConnectionService, we can rely on the
            // third-party app to display its own incoming call UI.
            incomingCall = null;
        }

        Log.i(this, "updateIncomingCall: foundIncomingcall = %s", incomingCall);

        boolean hadIncomingCall = mIncomingCall != null;
        boolean hasIncomingCall = incomingCall != null;
        if (incomingCall != mIncomingCall) {
            Call previousIncomingCall = mIncomingCall;
            mIncomingCall = incomingCall;

            if (hasIncomingCall && !hadIncomingCall) {
                mIncomingCall.addListener(mCallListener);
                showIncomingCallNotification(mIncomingCall);
            } else if (hadIncomingCall && !hasIncomingCall) {
                previousIncomingCall.removeListener(mCallListener);
                hideIncomingCallNotification();
            }
        }
    }

    private void showIncomingCallNotification(Call call) {
        Log.i(this, "showIncomingCallNotification showCall = %s", call);

        Notification.Builder builder = getNotificationBuilder(call,
                mCallsManagerProxy.getActiveCall());
        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL, builder.build());
    }

    private void hideIncomingCallNotification() {
        Log.i(this, "hideIncomingCallNotification");
        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL);
    }

    private String getNotificationName(Call call) {
        String name = "";
        if (call.getCallerDisplayNamePresentation() == TelecomManager.PRESENTATION_ALLOWED) {
            name = call.getCallerDisplayName();
        }
        if (TextUtils.isEmpty(name)) {
            name = call.getName();
        }

        if (TextUtils.isEmpty(name)) {
            name = call.getPhoneNumber();
        }
        return name;
    }

    private Notification.Builder getNotificationBuilder(Call incomingCall, Call ongoingCall) {
        // Change the notification app name to "Android System" to sufficiently distinguish this
        // from the phone app's name.
        Bundle extras = new Bundle();
        extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mContext.getString(
                com.android.internal.R.string.android_system_label));

        Intent answerIntent = new Intent(
                TelecomBroadcastIntentProcessor.ACTION_ANSWER_FROM_NOTIFICATION, null, mContext,
                TelecomBroadcastReceiver.class);
        Intent rejectIntent = new Intent(
                TelecomBroadcastIntentProcessor.ACTION_REJECT_FROM_NOTIFICATION, null, mContext,
                TelecomBroadcastReceiver.class);

        String nameOrNumber = getNotificationName(incomingCall);
        CharSequence viaApp = incomingCall.getTargetPhoneAccountLabel();
        boolean isIncomingVideo = VideoProfile.isVideo(incomingCall.getVideoState());
        boolean isOngoingVideo = ongoingCall != null ?
                VideoProfile.isVideo(ongoingCall.getVideoState()) : false;
        int numOtherCalls = ongoingCall != null ?
                mCallsManagerProxy.getNumUnholdableCallsForOtherConnectionService(
                        incomingCall.getTargetPhoneAccount()) : 1;

        // Build the "IncomingApp call from John Smith" message.
        CharSequence incomingCallText;
        if (isIncomingVideo) {
            incomingCallText = mContext.getString(R.string.notification_incoming_video_call, viaApp,
                    nameOrNumber);
        } else {
            incomingCallText = mContext.getString(R.string.notification_incoming_call, viaApp,
                    nameOrNumber);
        }

        // Build the "Answering will end your OtherApp call" line.
        CharSequence disconnectText;
        if (ongoingCall != null && ongoingCall.isSelfManaged()) {
            CharSequence ongoingApp = ongoingCall.getTargetPhoneAccountLabel();
            // For an ongoing self-managed call, we use a message like:
            // "Answering will end your OtherApp call".
            if (numOtherCalls > 1) {
                // Multiple ongoing calls in the other app, so don't bother specifing whether it is
                // a video call or audio call.
                disconnectText = mContext.getString(R.string.answering_ends_other_calls,
                        ongoingApp);
            } else if (isOngoingVideo) {
                disconnectText = mContext.getString(R.string.answering_ends_other_video_call,
                        ongoingApp);
            } else {
                disconnectText = mContext.getString(R.string.answering_ends_other_call, ongoingApp);
            }
        } else {
            // For an ongoing managed call, we use a message like:
            // "Answering will end your ongoing call".
            if (numOtherCalls > 1) {
                // Multiple ongoing manage calls, so don't bother specifing whether it is a video
                // call or audio call.
                disconnectText = mContext.getString(R.string.answering_ends_other_managed_calls);
            } else if (isOngoingVideo) {
                disconnectText = mContext.getString(
                        R.string.answering_ends_other_managed_video_call);
            } else {
                disconnectText = mContext.getString(R.string.answering_ends_other_managed_call);
            }
        }

        final Notification.Builder builder = new Notification.Builder(mContext);
        builder.setOngoing(true);
        builder.setExtras(extras);
        builder.setPriority(Notification.PRIORITY_HIGH);
        builder.setCategory(Notification.CATEGORY_CALL);
        builder.setContentTitle(incomingCallText);
        builder.setContentText(disconnectText);
        builder.setSmallIcon(R.drawable.ic_phone);
        builder.setChannelId(NotificationChannelManager.CHANNEL_ID_INCOMING_CALLS);
        // Ensures this is a heads up notification.  A heads-up notification is typically only shown
        // if there is a fullscreen intent.  However since this notification doesn't have that we
        // will use this trick to get it to show as one anyways.
        builder.setVibrate(new long[0]);
        builder.setColor(mContext.getResources().getColor(R.color.theme_color));
        builder.addAction(
                R.anim.on_going_call,
                getActionText(R.string.answer_incoming_call, R.color.notification_action_answer),
                PendingIntent.getBroadcast(mContext, 0, answerIntent,
                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE));
        builder.addAction(
                R.drawable.ic_close_dk,
                getActionText(R.string.decline_incoming_call, R.color.notification_action_decline),
                PendingIntent.getBroadcast(mContext, 0, rejectIntent,
                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE));
        return builder;
    }

    private CharSequence getActionText(int stringRes, int colorRes) {
        CharSequence string = mContext.getText(stringRes);
        if (string == null) {
            return "";
        }
        Spannable spannable = new SpannableString(string);
        spannable.setSpan(
                    new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
        return spannable;
    }
}