/*
 * Copyright (C) 2018 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.car.notification;

import android.app.Notification;
import android.content.Context;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;

import com.android.car.ui.recyclerview.ContentLimitingAdapter;

import java.util.List;
import java.util.Objects;

/**
 * {@link DiffUtil} for car notifications.
 * This class is not intended for general usage except for the static methods.
 *
 * <p> Two notifications are considered the same if they have the same:
 * <ol>
 * <li> GroupKey
 * <li> Number of AlertEntry contained
 * <li> The order of each AlertEntry
 * <li> The identifier of each individual AlertEntry contained
 * <li> The content of each individual AlertEntry contained
 * </ol>
 */
class CarNotificationDiff extends DiffUtil.Callback {
    private final Context mContext;
    private final List<NotificationGroup> mOldList;
    private final List<NotificationGroup> mNewList;
    private final int mMaxItems;
    private boolean mShowRecentsAndOlderHeaders;

    CarNotificationDiff(Context context, @NonNull List<NotificationGroup> oldList,
            @NonNull List<NotificationGroup> newList) {
        this(context, oldList, newList, ContentLimitingAdapter.UNLIMITED);
    }

    CarNotificationDiff(Context context, @NonNull List<NotificationGroup> oldList,
            @NonNull List<NotificationGroup> newList, int maxItems) {
        mContext = context;
        mOldList = oldList;
        mNewList = newList;
        mMaxItems = maxItems;
    }

    void setShowRecentsAndOlderHeaders(boolean showRecentsAndOlderHeaders) {
        mShowRecentsAndOlderHeaders = showRecentsAndOlderHeaders;
    }

    @Override
    public int getOldListSize() {
        return getContentLimitedListSize(mOldList.size());
    }

    @Override
    public int getNewListSize() {
        return getContentLimitedListSize(mNewList.size());
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        NotificationGroup oldItem = mOldList.get(oldItemPosition);
        NotificationGroup newItem = mNewList.get(newItemPosition);

        return sameGroupUniqueIdentifiers(oldItem, newItem, mShowRecentsAndOlderHeaders);
    }

    /**
     * Shallow comparison for {@link NotificationGroup}.
     * <p>
     * Checks if two grouped notifications have the same:
     * <ol>
     * <li> GroupKey
     * <li> GroupSummaryKey
     * </ol>
     * <p>
     * This method does not check for child AlertEntries because child itself will take care of it.
     *
     * @param showRecentsAndOlderHeaders if {@code true} then isSeen values of the two notification
     * groups are also compared.
     */
    static boolean sameGroupUniqueIdentifiers(NotificationGroup oldItem,
            NotificationGroup newItem, boolean showRecentsAndOlderHeaders) {

        if (oldItem == newItem) {
            return true;
        }

        if (!oldItem.getGroupKey().equals(newItem.getGroupKey())) {
            return false;
        }

        if (showRecentsAndOlderHeaders) {
            if (oldItem.isSeen() != newItem.isSeen()) {
                return false;
            }
        }

        return sameNotificationKey(
                oldItem.getGroupSummaryNotification(), newItem.getGroupSummaryNotification());
    }

    /**
     * Shallow comparison for {@link AlertEntry}: only comparing the unique IDs.
     *
     * <p> Returns true if two notifications have the same key.
     */
    static boolean sameNotificationKey(AlertEntry oldItem, AlertEntry newItem) {
        if (oldItem == newItem) {
            return true;
        }

        return oldItem != null
                && newItem != null
                && Objects.equals(oldItem.getKey(), newItem.getKey());
    }

    /**
     * Shallow comparison for {@link AlertEntry}: comparing the unique IDs and the
     * notification Flags.
     *
     * <p> Returns true if two notifications have the same key and notification flags.
     */
    static boolean sameNotificationKeyAndFlags(AlertEntry oldItem, AlertEntry newItem) {
        return sameNotificationKey(oldItem, newItem)
                && oldItem.getNotification().flags == newItem.getNotification().flags;
    }

    /**
     * Deep comparison for {@link NotificationGroup}.
     *
     * <p> Compare the size and contents of each AlertEntry inside the NotificationGroup.
     *
     * <p> This method will only be called if {@link #areItemsTheSame} returns true.
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        NotificationGroup oldItem = mOldList.get(oldItemPosition);
        NotificationGroup newItem = mNewList.get(newItemPosition);

        // Header and Footer should always refresh if some notification items have changed.
        if (newItem.isHeaderOrFooter()) {
            return false;
        }

        if (!sameNotificationContent(
                oldItem.getGroupSummaryNotification(), newItem.getGroupSummaryNotification())) {
            return false;
        }

        if (oldItem.getChildCount() != newItem.getChildCount()) {
            return false;
        }

        List<AlertEntry> oldChildNotifications = oldItem.getChildNotifications();
        List<AlertEntry> newChildNotifications = newItem.getChildNotifications();

        for (int i = 0; i < oldItem.getChildCount(); i++) {
            AlertEntry oldNotification = oldChildNotifications.get(i);
            AlertEntry newNotification = newChildNotifications.get(i);
            if (!sameNotificationContent(oldNotification, newNotification)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Deep comparison for {@link AlertEntry}.
     *
     * <p> We are only comparing a subset of the fields that have visible effects on our product.
     * Most of the deprecated fields are not compared.
     * Fields that do not have visible effects (e.g. privacy-related) are ignored for now.
     */
    private boolean sameNotificationContent(AlertEntry oldItem, AlertEntry newItem) {

        if (oldItem == newItem) {
            return true;
        }

        if (oldItem == null || newItem == null) {
            return false;
        }

        if (oldItem.getStatusBarNotification().isGroup()
                != newItem.getStatusBarNotification().isGroup()
                || oldItem.getStatusBarNotification().isClearable()
                != newItem.getStatusBarNotification().isClearable()
                || oldItem.getStatusBarNotification().isOngoing()
                != newItem.getStatusBarNotification().isOngoing()) {
            return false;
        }

        Notification oldNotification = oldItem.getNotification();
        Notification newNotification = newItem.getNotification();

        if (oldNotification.flags != newNotification.flags
                || oldNotification.category != newNotification.category
                || oldNotification.color != newNotification.color
                || !areBundlesEqual(oldNotification.extras, newNotification.extras)
                || !Objects.equals(oldNotification.contentIntent, newNotification.contentIntent)
                || !Objects.equals(oldNotification.deleteIntent, newNotification.deleteIntent)
                || !Objects.equals(
                oldNotification.fullScreenIntent, newNotification.fullScreenIntent)
                || !Objects.deepEquals(oldNotification.actions, newNotification.actions)) {
            return false;
        }

        // Recover builders only until the above if-statements fail
        Notification.Builder oldBuilder =
                Notification.Builder.recoverBuilder(mContext, oldNotification);
        Notification.Builder newBuilder =
                Notification.Builder.recoverBuilder(mContext, newNotification);

        return !Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder);
    }

    private boolean areBundlesEqual(Bundle oldBundle, Bundle newBundle) {
        if (oldBundle.size() != newBundle.size()) {
            return false;
        }

        for (String key : oldBundle.keySet()) {
            if (!newBundle.containsKey(key)) {
                return false;
            }

            Object oldValue = oldBundle.get(key);
            Object newValue = newBundle.get(key);
            if (!Objects.equals(oldValue, newValue)) {
                return false;
            }
        }

        return true;
    }

    private int getContentLimitedListSize(int listSize) {
        if (mMaxItems != ContentLimitingAdapter.UNLIMITED) {
            // Add one to mMaxItems to account for the scrolling limited message that is added by
            // the ContentLimitingAdapter.
            return Math.min(listSize, mMaxItems + 1);
        }
        return listSize;
    }
}
