/*
 * Copyright (C) 2019 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.systemui.statusbar.policy;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.util.Pair;
import android.widget.Button;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.systemui.Dependency;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Holder for inflated smart replies and actions. These objects should be inflated on a background
 * thread, to later be accessed and modified on the (performance critical) UI thread.
 */
public class InflatedSmartReplies {
    private static Context mContext;
    private static final String TAG = "InflatedSmartReplies";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    @Nullable private final SmartReplyView mSmartReplyView;
    @Nullable private final List<Button> mSmartSuggestionButtons;
    @NonNull private final SmartRepliesAndActions mSmartRepliesAndActions;
    private static boolean mSmartReplyHide;

    private InflatedSmartReplies(
            @Nullable SmartReplyView smartReplyView,
            @Nullable List<Button> smartSuggestionButtons,
            @NonNull SmartRepliesAndActions smartRepliesAndActions) {
        mSmartReplyView = smartReplyView;
        mSmartSuggestionButtons = smartSuggestionButtons;
        mSmartRepliesAndActions = smartRepliesAndActions;
    }

    @Nullable public SmartReplyView getSmartReplyView() {
        return mSmartReplyView;
    }

    @Nullable public List<Button> getSmartSuggestionButtons() {
        return mSmartSuggestionButtons;
    }

    @NonNull public SmartRepliesAndActions getSmartRepliesAndActions() {
        return mSmartRepliesAndActions;
    }

    /**
     * Inflate a SmartReplyView and its smart suggestions.
     */
    public static InflatedSmartReplies inflate(
            Context context,
            Context packageContext,
            NotificationEntry entry,
            SmartReplyConstants smartReplyConstants,
            SmartReplyController smartReplyController,
            HeadsUpManager headsUpManager,
            SmartRepliesAndActions existingSmartRepliesAndActions) {
        mContext = context;
        SmartRepliesAndActions newSmartRepliesAndActions =
                chooseSmartRepliesAndActions(smartReplyConstants, entry);
        if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
            return new InflatedSmartReplies(null /* smartReplyView */,
                    null /* smartSuggestionButtons */, newSmartRepliesAndActions);
        }

        // Only block clicks if the smart buttons are different from the previous set - to avoid
        // scenarios where a user incorrectly cannot click smart buttons because the notification is
        // updated.
        boolean delayOnClickListener =
                !areSuggestionsSimilar(existingSmartRepliesAndActions, newSmartRepliesAndActions);

        SmartReplyView smartReplyView = SmartReplyView.inflate(context);

        List<Button> suggestionButtons = new ArrayList<>();
        if (newSmartRepliesAndActions.smartReplies != null) {
            suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
                    newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
                    delayOnClickListener));
        }
        if (newSmartRepliesAndActions.smartActions != null) {
            suggestionButtons.addAll(
                    smartReplyView.inflateSmartActions(packageContext,
                            newSmartRepliesAndActions.smartActions, smartReplyController, entry,
                            headsUpManager, delayOnClickListener));
        }

        return new InflatedSmartReplies(smartReplyView, suggestionButtons,
                newSmartRepliesAndActions);
    }

    @VisibleForTesting
    static boolean areSuggestionsSimilar(
            SmartRepliesAndActions left, SmartRepliesAndActions right) {
        if (left == right) return true;
        if (left == null || right == null) return false;

        if (!Arrays.equals(left.getSmartReplies(), right.getSmartReplies())) {
            return false;
        }

        return !NotificationUiAdjustment.areDifferent(
                left.getSmartActions(), right.getSmartActions());
    }

    /**
     * Returns whether we should show the smart reply view and its smart suggestions.
     */
    public static boolean shouldShowSmartReplyView(
            NotificationEntry entry,
            SmartRepliesAndActions smartRepliesAndActions) {
        if (smartRepliesAndActions.smartReplies == null
                && smartRepliesAndActions.smartActions == null) {
            // There are no smart replies and no smart actions.
            return false;
        }
        // If we are showing the spinner we don't want to add the buttons.
        boolean showingSpinner = entry.notification.getNotification()
                .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
        if (showingSpinner) {
            return false;
        }
        // If we are keeping the notification around while sending we don't want to add the buttons.
        boolean hideSmartReplies = entry.notification.getNotification()
                .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
        if (hideSmartReplies) {
            return false;
        }
        return true;
    }

    private static void updateSmartReplyVisibility() {
        mSmartReplyHide = Settings.System.getIntForUser(mContext.getContentResolver(),
                Settings.System.HIDE_SMART_REPLIES, 0, UserHandle.USER_CURRENT) != 0;
    }

    public void onSettingsChanged() {
        updateSmartReplyVisibility();
    }

    /**
     * Chose what smart replies and smart actions to display. App generated suggestions take
     * precedence. So if the app provides any smart replies, we don't show any
     * replies or actions generated by the NotificationAssistantService (NAS), and if the app
     * provides any smart actions we also don't show any NAS-generated replies or actions.
     */
    @NonNull
    public static SmartRepliesAndActions chooseSmartRepliesAndActions(
            SmartReplyConstants smartReplyConstants,
            final NotificationEntry entry) {
        Notification notification = entry.notification.getNotification();
        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
                notification.findRemoteInputActionPair(false /* freeform */);
        Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
                notification.findRemoteInputActionPair(true /* freeform */);

        updateSmartReplyVisibility();
        if (!smartReplyConstants.isEnabled() || mSmartReplyHide) {
            if (DEBUG) {
                Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
                        + entry.notification.getKey());
            }
            return new SmartRepliesAndActions(null, null);
        }
        // Only use smart replies from the app if they target P or above. We have this check because
        // the smart reply API has been used for other things (Wearables) in the past. The API to
        // add smart actions is new in Q so it doesn't require a target-sdk check.
        boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
                || entry.targetSdk >= Build.VERSION_CODES.P);

        boolean appGeneratedSmartRepliesExist =
                enableAppGeneratedSmartReplies
                        && remoteInputActionPair != null
                        && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
                        && remoteInputActionPair.second.actionIntent != null;

        List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
        boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();

        SmartReplyView.SmartReplies smartReplies = null;
        SmartReplyView.SmartActions smartActions = null;
        if (appGeneratedSmartRepliesExist) {
            smartReplies = new SmartReplyView.SmartReplies(
                    remoteInputActionPair.first.getChoices(),
                    remoteInputActionPair.first,
                    remoteInputActionPair.second.actionIntent,
                    false /* fromAssistant */);
        }
        if (appGeneratedSmartActionsExist) {
            smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
                    false /* fromAssistant */);
        }
        // Apps didn't provide any smart replies / actions, use those from NAS (if any).
        if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
            boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.systemGeneratedSmartReplies)
                    && freeformRemoteInputActionPair != null
                    && freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
                    && freeformRemoteInputActionPair.second.actionIntent != null;
            if (useGeneratedReplies) {
                smartReplies = new SmartReplyView.SmartReplies(
                        entry.systemGeneratedSmartReplies,
                        freeformRemoteInputActionPair.first,
                        freeformRemoteInputActionPair.second.actionIntent,
                        true /* fromAssistant */);
            }
            boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
                    && notification.getAllowSystemGeneratedContextualActions();
            if (useSmartActions) {
                List<Notification.Action> systemGeneratedActions =
                        entry.systemGeneratedSmartActions;
                // Filter actions if we're in kiosk-mode - we don't care about screen pinning mode,
                // since notifications aren't shown there anyway.
                ActivityManagerWrapper activityManagerWrapper =
                        Dependency.get(ActivityManagerWrapper.class);
                if (activityManagerWrapper.isLockTaskKioskModeActive()) {
                    systemGeneratedActions = filterWhiteListedLockTaskApps(systemGeneratedActions);
                }
                smartActions = new SmartReplyView.SmartActions(
                        systemGeneratedActions, true /* fromAssistant */);
            }
        }
        return new SmartRepliesAndActions(smartReplies, smartActions);
    }

    /**
     * Filter actions so that only actions pointing to whitelisted apps are allowed.
     * This filtering is only meaningful when in lock-task mode.
     */
    private static List<Notification.Action> filterWhiteListedLockTaskApps(
            List<Notification.Action> actions) {
        PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class);
        DevicePolicyManagerWrapper devicePolicyManagerWrapper =
                Dependency.get(DevicePolicyManagerWrapper.class);
        List<Notification.Action> filteredActions = new ArrayList<>();
        for (Notification.Action action : actions) {
            if (action.actionIntent == null) continue;
            Intent intent = action.actionIntent.getIntent();
            //  Only allow actions that are explicit (implicit intents are not handled in lock-task
            //  mode), and link to whitelisted apps.
            ResolveInfo resolveInfo = packageManagerWrapper.resolveActivity(intent, 0 /* flags */);
            if (resolveInfo != null && devicePolicyManagerWrapper.isLockTaskPermitted(
                    resolveInfo.activityInfo.packageName)) {
                filteredActions.add(action);
            }
        }
        return filteredActions;
    }

    /**
     * Returns whether the {@link Notification} represented by entry has a free-form remote input.
     * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
     * through the remote input.
     */
    public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
        Notification notification = entry.notification.getNotification();
        return null != notification.findRemoteInputActionPair(true /* freeform */);
    }

    /**
     * A storage for smart replies and smart action.
     */
    public static class SmartRepliesAndActions {
        @Nullable public final SmartReplyView.SmartReplies smartReplies;
        @Nullable public final SmartReplyView.SmartActions smartActions;

        SmartRepliesAndActions(
                @Nullable SmartReplyView.SmartReplies smartReplies,
                @Nullable SmartReplyView.SmartActions smartActions) {
            this.smartReplies = smartReplies;
            this.smartActions = smartActions;
        }

        @NonNull public CharSequence[] getSmartReplies() {
            return smartReplies == null ? new CharSequence[0] : smartReplies.choices;
        }

        @NonNull public List<Notification.Action> getSmartActions() {
            return smartActions == null ? Collections.emptyList() : smartActions.actions;
        }
    }
}
