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

import static android.Manifest.permission.READ_CONTACTS;
import static android.app.Notification.CATEGORY_MISSED_CALL;
import static android.app.Notification.EXTRA_MESSAGES;
import static android.app.Notification.EXTRA_PEOPLE_LIST;

import android.annotation.Nullable;
import android.app.Notification;
import android.app.Person;
import android.content.pm.PackageManager;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.wm.shell.bubbles.Bubbles;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/** Helper functions to handle notifications in People Tiles. */
public class NotificationHelper {
    private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
    private static final String TAG = "PeopleNotifHelper";

    /** Returns the notification with highest priority to be shown in People Tiles. */
    public static NotificationEntry getHighestPriorityNotification(
            Set<NotificationEntry> notificationEntries) {
        if (notificationEntries == null || notificationEntries.isEmpty()) {
            return null;
        }

        return notificationEntries
                .stream()
                .filter(NotificationHelper::isMissedCallOrHasContent)
                .sorted(notificationEntryComparator)
                .findFirst().orElse(null);
    }


    /** Notification comparator, checking category and timestamps, in reverse order of priority. */
    public static Comparator<NotificationEntry> notificationEntryComparator =
            new Comparator<NotificationEntry>() {
                @Override
                public int compare(NotificationEntry e1, NotificationEntry e2) {
                    Notification n1 = e1.getSbn().getNotification();
                    Notification n2 = e2.getSbn().getNotification();

                    boolean missedCall1 = isMissedCall(n1);
                    boolean missedCall2 = isMissedCall(n2);
                    if (missedCall1 && !missedCall2) {
                        return -1;
                    }
                    if (!missedCall1 && missedCall2) {
                        return 1;
                    }

                    // Get messages in reverse chronological order.
                    List<Notification.MessagingStyle.Message> messages1 =
                            getMessagingStyleMessages(n1);
                    List<Notification.MessagingStyle.Message> messages2 =
                            getMessagingStyleMessages(n2);

                    if (messages1 != null && messages2 != null) {
                        Notification.MessagingStyle.Message message1 = messages1.get(0);
                        Notification.MessagingStyle.Message message2 = messages2.get(0);
                        return (int) (message2.getTimestamp() - message1.getTimestamp());
                    }

                    if (messages1 == null) {
                        return 1;
                    }
                    if (messages2 == null) {
                        return -1;
                    }
                    return (int) (n2.getWhen() - n1.getWhen());
                }
            };

    /** Returns whether {@code e} is a missed call notification. */
    public static boolean isMissedCall(NotificationEntry e) {
        return e != null && e.getSbn().getNotification() != null
                && isMissedCall(e.getSbn().getNotification());
    }

    /** Returns whether {@code notification} is a missed call notification. */
    public static boolean isMissedCall(Notification notification) {
        return notification != null && Objects.equals(notification.category, CATEGORY_MISSED_CALL);
    }

    private static boolean hasContent(NotificationEntry e) {
        if (e == null) {
            return false;
        }
        List<Notification.MessagingStyle.Message> messages =
                getMessagingStyleMessages(e.getSbn().getNotification());
        return messages != null && !messages.isEmpty();
    }

    /** Returns whether {@code e} is a valid conversation notification. */
    public static boolean isValid(NotificationEntry e) {
        return e != null && e.getRanking() != null
                && e.getRanking().getConversationShortcutInfo() != null
                && e.getSbn().getNotification() != null;
    }

    /** Returns whether conversation notification should be shown in People Tile. */
    public static boolean isMissedCallOrHasContent(NotificationEntry e) {
        return isMissedCall(e) || hasContent(e);
    }

    /** Returns whether {@code sbn}'s package has permission to read contacts. */
    public static boolean hasReadContactsPermission(
            PackageManager packageManager, StatusBarNotification sbn) {
        return packageManager.checkPermission(READ_CONTACTS,
                sbn.getPackageName()) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Returns whether a notification should be matched to other Tiles by Uri.
     *
     * <p>Currently only matches missed calls.
     */
    public static boolean shouldMatchNotificationByUri(StatusBarNotification sbn) {
        Notification notification = sbn.getNotification();
        if (notification == null) {
            if (DEBUG) Log.d(TAG, "Notification is null");
            return false;
        }
        boolean isMissedCall = isMissedCall(notification);
        if (!isMissedCall) {
            if (DEBUG) Log.d(TAG, "Not missed call");
        }
        return isMissedCall;
    }

    /**
     * Try to retrieve a valid Uri via {@code sbn}, falling back to the {@code
     * contactUriFromShortcut} if valid.
     */
    @Nullable
    public static String getContactUri(StatusBarNotification sbn) {
        // First, try to get a Uri from the Person directly set on the Notification.
        ArrayList<Person> people = sbn.getNotification().extras.getParcelableArrayList(
                EXTRA_PEOPLE_LIST);
        if (people != null && people.get(0) != null) {
            String contactUri = people.get(0).getUri();
            if (contactUri != null && !contactUri.isEmpty()) {
                return contactUri;
            }
        }

        // Then, try to get a Uri from the Person set on the Notification message.
        List<Notification.MessagingStyle.Message> messages =
                getMessagingStyleMessages(sbn.getNotification());
        if (messages != null && !messages.isEmpty()) {
            Notification.MessagingStyle.Message message = messages.get(0);
            Person sender = message.getSenderPerson();
            if (sender != null && sender.getUri() != null && !sender.getUri().isEmpty()) {
                return sender.getUri();
            }
        }

        return null;
    }

    /**
     * Returns {@link Notification.MessagingStyle.Message}s from the Notification in chronological
     * order from most recent to least.
     */
    @VisibleForTesting
    @Nullable
    public static List<Notification.MessagingStyle.Message> getMessagingStyleMessages(
            Notification notification) {
        if (notification == null) {
            return null;
        }
        if (notification.isStyle(Notification.MessagingStyle.class)
                && notification.extras != null) {
            final Parcelable[] messages = notification.extras.getParcelableArray(EXTRA_MESSAGES);
            if (!ArrayUtils.isEmpty(messages)) {
                List<Notification.MessagingStyle.Message> sortedMessages =
                        Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
                sortedMessages.sort(Collections.reverseOrder(
                        Comparator.comparing(Notification.MessagingStyle.Message::getTimestamp)));
                return sortedMessages;
            }
        }
        return null;
    }

    /** Returns whether {@code notification} is a group conversation. */
    private static boolean isGroupConversation(Notification notification) {
        return notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION, false);
    }

    /**
     * Returns {@code message}'s sender's name if {@code notification} is from a group conversation.
     */
    public static CharSequence getSenderIfGroupConversation(Notification notification,
            Notification.MessagingStyle.Message message) {
        if (!isGroupConversation(notification)) {
            if (DEBUG) {
                Log.d(TAG, "Notification is not from a group conversation, not checking sender.");
            }
            return null;
        }
        Person person = message.getSenderPerson();
        if (person == null) {
            if (DEBUG) Log.d(TAG, "Notification from group conversation doesn't include sender.");
            return null;
        }
        if (DEBUG) Log.d(TAG, "Returning sender from group conversation notification.");
        return person.getName();
    }

    /** Returns whether {@code entry} is suppressed from shade, meaning we should not show it. */
    public static boolean shouldFilterOut(
            Optional<Bubbles> bubblesOptional, NotificationEntry entry) {
        boolean isSuppressed = false;
        //TODO(b/190822282): Investigate what is causing the NullPointerException
        try {
            isSuppressed = bubblesOptional.isPresent()
                    && bubblesOptional.get().isBubbleNotificationSuppressedFromShade(
                    entry.getKey(), entry.getSbn().getGroupKey());
        } catch (Exception e) {
            Log.e(TAG, "Exception checking if notification is suppressed: " + e);
        }
        return isSuppressed;
    }
}

