/*
 * 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.notification.collection;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Person;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dependency;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.notification.NotificationFilter;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * The list of currently displaying notifications.
 */
public class NotificationData {

    private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class);

    /**
     * These dependencies are late init-ed
     */
    private KeyguardEnvironment mEnvironment;
    private NotificationMediaManager mMediaManager;

    private HeadsUpManager mHeadsUpManager;

    private final ArrayMap<String, NotificationEntry> mEntries = new ArrayMap<>();
    private final ArrayList<NotificationEntry> mSortedAndFiltered = new ArrayList<>();

    private final NotificationGroupManager mGroupManager =
            Dependency.get(NotificationGroupManager.class);

    private RankingMap mRankingMap;
    private final Ranking mTmpRanking = new Ranking();

    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
        mHeadsUpManager = headsUpManager;
    }

    @VisibleForTesting
    protected final Comparator<NotificationEntry> mRankingComparator =
            new Comparator<NotificationEntry>() {
        private final Ranking mRankingA = new Ranking();
        private final Ranking mRankingB = new Ranking();

        @Override
        public int compare(NotificationEntry a, NotificationEntry b) {
            final StatusBarNotification na = a.notification;
            final StatusBarNotification nb = b.notification;
            int aImportance = NotificationManager.IMPORTANCE_DEFAULT;
            int bImportance = NotificationManager.IMPORTANCE_DEFAULT;
            int aRank = 0;
            int bRank = 0;

            if (mRankingMap != null) {
                // RankingMap as received from NoMan
                getRanking(a.key, mRankingA);
                getRanking(b.key, mRankingB);
                aImportance = mRankingA.getImportance();
                bImportance = mRankingB.getImportance();
                aRank = mRankingA.getRank();
                bRank = mRankingB.getRank();
            }

            String mediaNotification = getMediaManager().getMediaNotificationKey();

            // IMPORTANCE_MIN media streams are allowed to drift to the bottom
            final boolean aMedia = a.key.equals(mediaNotification)
                    && aImportance > NotificationManager.IMPORTANCE_MIN;
            final boolean bMedia = b.key.equals(mediaNotification)
                    && bImportance > NotificationManager.IMPORTANCE_MIN;

            boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH
                    && isSystemNotification(na);
            boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH
                    && isSystemNotification(nb);


            boolean aHeadsUp = a.getRow().isHeadsUp();
            boolean bHeadsUp = b.getRow().isHeadsUp();

            // HACK: This should really go elsewhere, but it's currently not straightforward to
            // extract the comparison code and we're guaranteed to touch every element, so this is
            // the best place to set the buckets for the moment.
            a.setIsTopBucket(aHeadsUp || aMedia || aSystemMax || a.isHighPriority());
            b.setIsTopBucket(bHeadsUp || bMedia || bSystemMax || b.isHighPriority());

            if (aHeadsUp != bHeadsUp) {
                return aHeadsUp ? -1 : 1;
            } else if (aHeadsUp) {
                // Provide consistent ranking with headsUpManager
                return mHeadsUpManager.compare(a, b);
            } else if (aMedia != bMedia) {
                // Upsort current media notification.
                return aMedia ? -1 : 1;
            } else if (aSystemMax != bSystemMax) {
                // Upsort PRIORITY_MAX system notifications
                return aSystemMax ? -1 : 1;
            } else if (a.isHighPriority() != b.isHighPriority()) {
                return -1 * Boolean.compare(a.isHighPriority(), b.isHighPriority());
            } else if (aRank != bRank) {
                return aRank - bRank;
            } else {
                return Long.compare(nb.getNotification().when, na.getNotification().when);
            }
        }
    };

    private KeyguardEnvironment getEnvironment() {
        if (mEnvironment == null) {
            mEnvironment = Dependency.get(KeyguardEnvironment.class);
        }
        return mEnvironment;
    }

    private NotificationMediaManager getMediaManager() {
        if (mMediaManager == null) {
            mMediaManager = Dependency.get(NotificationMediaManager.class);
        }
        return mMediaManager;
    }

    /**
     * Returns the sorted list of active notifications (depending on {@link KeyguardEnvironment}
     *
     * <p>
     * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
     * when the environment changes.
     * <p>
     * Don't hold on to or modify the returned list.
     */
    public ArrayList<NotificationEntry> getActiveNotifications() {
        return mSortedAndFiltered;
    }

    public ArrayList<NotificationEntry> getNotificationsForCurrentUser() {
        synchronized (mEntries) {
            final int len = mEntries.size();
            ArrayList<NotificationEntry> filteredForUser = new ArrayList<>(len);

            for (int i = 0; i < len; i++) {
                NotificationEntry entry = mEntries.valueAt(i);
                final StatusBarNotification sbn = entry.notification;
                if (!getEnvironment().isNotificationForCurrentProfiles(sbn)) {
                    continue;
                }
                filteredForUser.add(entry);
            }
            return filteredForUser;
        }
    }

    /**
     * Return whether there are any visible notifications (i.e. without an error).
     */
    public boolean hasActiveVisibleNotifications() {
        for (NotificationEntry e : mSortedAndFiltered) {
            if (e.getContentView() != null) { // the view successfully inflated
                return true;
            }
        }
        return false;
    }

    /**
     * Return whether there are any ongoing notifications (that aren't errors).
     */
    public boolean hasActiveOngoingNotifications() {
        for (NotificationEntry e : mSortedAndFiltered) {
            if (e.getContentView() != null) { // the view successfully inflated
                if (e.notification.isOngoing()) {
                    return true;
                }
            }
        }
        return false;
    }

    public NotificationEntry get(String key) {
        return mEntries.get(key);
    }

    public ArrayList<NotificationEntry> getAllNotificationsForPackage(String pkg) {
        synchronized (mEntries) {
            final int len = mEntries.size();
            ArrayList<NotificationEntry> filtered = new ArrayList<>(len);
            for (int i = 0; i < len; i++) {
                NotificationEntry entry = mEntries.valueAt(i);
                if (pkg.equals(entry.notification.getPackageName())) {
                    filtered.add(entry);
                }
            }
            return filtered;
        }
    }

    public void add(NotificationEntry entry) {
        synchronized (mEntries) {
            mEntries.put(entry.notification.getKey(), entry);
        }
        mGroupManager.onEntryAdded(entry);

        updateRankingAndSort(mRankingMap);
    }

    public NotificationEntry remove(String key, RankingMap ranking) {
        NotificationEntry removed;
        synchronized (mEntries) {
            removed = mEntries.remove(key);
        }
        if (removed == null) return null;
        // NEM may pass us a null ranking map if removing a lifetime-extended notification,
        // so use the most recent ranking
        if (ranking == null) ranking = mRankingMap;
        mGroupManager.onEntryRemoved(removed);
        updateRankingAndSort(ranking);
        return removed;
    }

    /** Updates the given notification entry with the provided ranking. */
    public void update(
            NotificationEntry entry,
            RankingMap ranking,
            StatusBarNotification notification) {
        updateRanking(ranking);
        final StatusBarNotification oldNotification = entry.notification;
        entry.notification = notification;
        mGroupManager.onEntryUpdated(entry, oldNotification);
    }

    public void updateRanking(RankingMap ranking) {
        updateRankingAndSort(ranking);
    }

    public void updateAppOp(int appOp, int uid, String pkg, String key, boolean showIcon) {
        synchronized (mEntries) {
            final int len = mEntries.size();
            for (int i = 0; i < len; i++) {
                NotificationEntry entry = mEntries.valueAt(i);
                if (uid == entry.notification.getUid()
                        && pkg.equals(entry.notification.getPackageName())
                        && key.equals(entry.key)) {
                    if (showIcon) {
                        entry.mActiveAppOps.add(appOp);
                    } else {
                        entry.mActiveAppOps.remove(appOp);
                    }
                }
            }
        }
    }

    /**
     * Returns true if this notification should be displayed in the high-priority notifications
     * section
     */
    public boolean isHighPriority(StatusBarNotification statusBarNotification) {
        if (mRankingMap != null) {
            getRanking(statusBarNotification.getKey(), mTmpRanking);
            if (mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT
                    || hasHighPriorityCharacteristics(
                            mTmpRanking.getChannel(), statusBarNotification)) {
                return true;
            }
            if (mGroupManager.isSummaryOfGroup(statusBarNotification)) {
                final ArrayList<NotificationEntry> logicalChildren =
                        mGroupManager.getLogicalChildren(statusBarNotification);
                for (NotificationEntry child : logicalChildren) {
                    if (isHighPriority(child.notification)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean hasHighPriorityCharacteristics(NotificationChannel channel,
            StatusBarNotification statusBarNotification) {

        if (isImportantOngoing(statusBarNotification.getNotification())
                || statusBarNotification.getNotification().hasMediaSession()
                || hasPerson(statusBarNotification.getNotification())
                || hasStyle(statusBarNotification.getNotification(),
                Notification.MessagingStyle.class)) {
            // Users who have long pressed and demoted to silent should not see the notification
            // in the top section
            if (channel != null && channel.hasUserSetImportance()) {
                return false;
            }
            return true;
        }

        return false;
    }

    private boolean isImportantOngoing(Notification notification) {
        return notification.isForegroundService()
                && mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_LOW;
    }

    private boolean hasStyle(Notification notification, Class targetStyle) {
        Class<? extends Notification.Style> style = notification.getNotificationStyle();
        return targetStyle.equals(style);
    }

    private boolean hasPerson(Notification notification) {
        // TODO: cache favorite and recent contacts to check contact affinity
        ArrayList<Person> people = notification.extras != null
                ? notification.extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST)
                : new ArrayList<>();
        return people != null && !people.isEmpty();
    }

    public boolean isAmbient(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.isAmbient();
        }
        return false;
    }

    public int getVisibilityOverride(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.getVisibilityOverride();
        }
        return Ranking.VISIBILITY_NO_OVERRIDE;
    }

    public int getImportance(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.getImportance();
        }
        return NotificationManager.IMPORTANCE_UNSPECIFIED;
    }

    public String getOverrideGroupKey(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.getOverrideGroupKey();
        }
        return null;
    }

    public List<SnoozeCriterion> getSnoozeCriteria(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.getSnoozeCriteria();
        }
        return null;
    }

    public NotificationChannel getChannel(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.getChannel();
        }
        return null;
    }

    public int getRank(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.getRank();
        }
        return 0;
    }

    public boolean shouldHide(String key) {
        if (mRankingMap != null) {
            getRanking(key, mTmpRanking);
            return mTmpRanking.isSuspended();
        }
        return false;
    }

    private void updateRankingAndSort(RankingMap ranking) {
        if (ranking != null) {
            mRankingMap = ranking;
            synchronized (mEntries) {
                final int len = mEntries.size();
                for (int i = 0; i < len; i++) {
                    NotificationEntry entry = mEntries.valueAt(i);
                    if (!getRanking(entry.key, mTmpRanking)) {
                        continue;
                    }
                    final StatusBarNotification oldSbn = entry.notification.cloneLight();
                    final String overrideGroupKey = getOverrideGroupKey(entry.key);
                    if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
                        entry.notification.setOverrideGroupKey(overrideGroupKey);
                        mGroupManager.onEntryUpdated(entry, oldSbn);
                    }
                    entry.populateFromRanking(mTmpRanking);
                    entry.setIsHighPriority(isHighPriority(entry.notification));
                }
            }
        }
        filterAndSort();
    }

    /**
     * Get the ranking from the current ranking map.
     *
     * @param key the key to look up
     * @param outRanking the ranking to populate
     *
     * @return {@code true} if the ranking was properly obtained.
     */
    @VisibleForTesting
    protected boolean getRanking(String key, Ranking outRanking) {
        return mRankingMap.getRanking(key, outRanking);
    }

    // TODO: This should not be public. Instead the Environment should notify this class when
    // anything changed, and this class should call back the UI so it updates itself.
    public void filterAndSort() {
        mSortedAndFiltered.clear();

        synchronized (mEntries) {
            final int len = mEntries.size();
            for (int i = 0; i < len; i++) {
                NotificationEntry entry = mEntries.valueAt(i);

                if (mNotificationFilter.shouldFilterOut(entry)) {
                    continue;
                }

                mSortedAndFiltered.add(entry);
            }
        }

        if (mSortedAndFiltered.size() == 1) {
            // HACK: We need the comparator to run on all children in order to set the
            // isHighPriority field. If there is only one child, then the comparison won't be run,
            // so we have to trigger it manually. Get rid of this code as soon as possible.
            mRankingComparator.compare(mSortedAndFiltered.get(0), mSortedAndFiltered.get(0));
        } else {
            Collections.sort(mSortedAndFiltered, mRankingComparator);
        }
    }

    public void dump(PrintWriter pw, String indent) {
        int filteredLen = mSortedAndFiltered.size();
        pw.print(indent);
        pw.println("active notifications: " + filteredLen);
        int active;
        for (active = 0; active < filteredLen; active++) {
            NotificationEntry e = mSortedAndFiltered.get(active);
            dumpEntry(pw, indent, active, e);
        }
        synchronized (mEntries) {
            int totalLen = mEntries.size();
            pw.print(indent);
            pw.println("inactive notifications: " + (totalLen - active));
            int inactiveCount = 0;
            for (int i = 0; i < totalLen; i++) {
                NotificationEntry entry = mEntries.valueAt(i);
                if (!mSortedAndFiltered.contains(entry)) {
                    dumpEntry(pw, indent, inactiveCount, entry);
                    inactiveCount++;
                }
            }
        }
    }

    private void dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e) {
        getRanking(e.key, mTmpRanking);
        pw.print(indent);
        pw.println("  [" + i + "] key=" + e.key + " icon=" + e.icon);
        StatusBarNotification n = e.notification;
        pw.print(indent);
        pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " importance="
                + mTmpRanking.getImportance());
        pw.print(indent);
        pw.println("      notification=" + n.getNotification());
    }

    private static boolean isSystemNotification(StatusBarNotification sbn) {
        String sbnPackage = sbn.getPackageName();
        return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
    }

    /**
     * Provides access to keyguard state and user settings dependent data.
     */
    public interface KeyguardEnvironment {
        boolean isDeviceProvisioned();
        boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
    }
}
