/*
 * Copyright (C) 2020 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;

import static android.app.Flags.notificationsRedesignAppIcons;
import static android.app.Flags.notificationsRedesignTemplates;

import android.app.Flags;
import android.app.Notification;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.NotificationHeaderView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.VisibleForTesting;

import com.android.internal.R;
import com.android.internal.widget.CachingIconView;
import com.android.internal.widget.ConversationLayout;
import com.android.internal.widget.ImageFloatingTextView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationContentView;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;

/**
 * A utility to manage notification views when they are placed in a group by adjusting elements
 * to reduce redundancies and occasionally tweak layouts to highlight the unique content.
 */
public class NotificationGroupingUtil {

    private static final TextViewComparator TEXT_VIEW_COMPARATOR = new TextViewComparator();
    private static final TextViewComparator APP_NAME_COMPARATOR = new AppNameComparator();
    private static final ViewComparator BADGE_COMPARATOR = new BadgeComparator();
    private static final VisibilityApplicator VISIBILITY_APPLICATOR = new VisibilityApplicator();
    private static final VisibilityApplicator APP_NAME_APPLICATOR = new AppNameApplicator();
    private static final ResultApplicator LEFT_ICON_APPLICATOR = new LeftIconApplicator();

    @VisibleForTesting
    static final DataExtractor ICON_EXTRACTOR = new DataExtractor() {
        @Override
        public Object extractData(ExpandableNotificationRow row) {
            if (NotificationBundleUi.isEnabled()) {
                if (row.getEntryAdapter().getSbn() != null) {
                    return row.getEntryAdapter().getSbn().getNotification();
                }
                return null;
            } else {
                return row.getEntryLegacy().getSbn().getNotification();
            }
        }
    };

    private final ExpandableNotificationRow mRow;
    private final ArrayList<Processor> mProcessors = new ArrayList<>();
    private final HashSet<Integer> mDividers = new HashSet<>();

    public NotificationGroupingUtil(ExpandableNotificationRow row) {
        mRow = row;

        final IconComparator iconVisibilityComparator = new IconComparator() {
            @Override
            public boolean compare(View parent, View child, Object parentData,
                    Object childData) {
                if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
                    // Icon is always the same when we're showing the app icon.
                    return true;
                }
                return hasSameIcon(parentData, childData)
                        && hasSameColor(parentData, childData);
            }
        };
        final IconComparator greyComparator = new IconComparator() {
            @Override
            public boolean compare(View parent, View child, Object parentData,
                    Object childData) {
                if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
                    return false;
                }
                return !hasSameIcon(parentData, childData)
                        || hasSameColor(parentData, childData);
            }
        };
        final ResultApplicator greyApplicator = new ResultApplicator() {
            @Override
            public void apply(View parent, View view, boolean apply, boolean reset) {
                if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
                    // Do nothing.
                    return;
                }
                CachingIconView icon = view.findViewById(com.android.internal.R.id.icon);
                if (icon != null) {
                    icon.setGrayedOut(apply);
                }
            }
        };

        // To hide the icons if they are the same and the color is the same
        mProcessors.add(new Processor(mRow,
                com.android.internal.R.id.icon,
                ICON_EXTRACTOR,
                iconVisibilityComparator,
                VISIBILITY_APPLICATOR));
        if (!notificationsRedesignAppIcons()) {
            // To grey out the icons when they are not the same, or they have the same color
            mProcessors.add(new Processor(mRow,
                    com.android.internal.R.id.status_bar_latest_event_content,
                    ICON_EXTRACTOR,
                    greyComparator,
                    greyApplicator));
        }
        // To show the large icon on the left side instead if all the small icons are the same
        mProcessors.add(new Processor(mRow,
                com.android.internal.R.id.status_bar_latest_event_content,
                ICON_EXTRACTOR,
                iconVisibilityComparator,
                LEFT_ICON_APPLICATOR));
        // To only show the work profile icon in the group header
        mProcessors.add(new Processor(mRow,
                com.android.internal.R.id.profile_badge,
                null /* Extractor */,
                BADGE_COMPARATOR,
                VISIBILITY_APPLICATOR));
        // To hide the app name in group children
        mProcessors.add(new Processor(mRow,
                com.android.internal.R.id.app_name_text,
                null,
                APP_NAME_COMPARATOR,
                APP_NAME_APPLICATOR));
        // To hide the header text if it's the same
        mProcessors.add(Processor.forTextView(mRow, com.android.internal.R.id.header_text));

        mDividers.add(com.android.internal.R.id.header_text_divider);
        mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
        mDividers.add(com.android.internal.R.id.time_divider);
    }

    /**
     * Update the appearance of the children in this group to reduce redundancies.
     */
    public void updateChildrenAppearance() {
        List<ExpandableNotificationRow> notificationChildren = mRow.getAttachedChildren();
        if (notificationChildren == null || !mRow.isSummaryWithChildren()) {
            return;
        }
        // Initialize the processors
        for (int compI = 0; compI < mProcessors.size(); compI++) {
            mProcessors.get(compI).init();
        }

        // Compare all notification headers
        for (int i = 0; i < notificationChildren.size(); i++) {
            ExpandableNotificationRow row = notificationChildren.get(i);
            for (int compI = 0; compI < mProcessors.size(); compI++) {
                mProcessors.get(compI).compareToGroupParent(row);
            }
        }

        // Apply the comparison to the row
        for (int i = 0; i < notificationChildren.size(); i++) {
            ExpandableNotificationRow row = notificationChildren.get(i);
            for (int compI = 0; compI < mProcessors.size(); compI++) {
                mProcessors.get(compI).apply(row);
            }
            // We need to sanitize the dividers since they might be off-balance now
            sanitizeTopLineViews(row);
        }
    }

    private void sanitizeTopLineViews(ExpandableNotificationRow row) {
        if (row.isSummaryWithChildren()) {
            sanitizeTopLine(row.getNotificationViewWrapper().getNotificationHeader(), row);
            return;
        }
        final NotificationContentView layout = row.getPrivateLayout();
        sanitizeChild(layout.getContractedChild(), row);
        sanitizeChild(layout.getHeadsUpChild(), row);
        sanitizeChild(layout.getExpandedChild(), row);
    }

    private void sanitizeChild(View child, ExpandableNotificationRow row) {
        if (child != null) {
            sanitizeTopLine(child.findViewById(R.id.notification_top_line), row);
        }
    }

    private void sanitizeTopLine(ViewGroup rowHeader, ExpandableNotificationRow row) {
        if (rowHeader == null) {
            return;
        }
        final int childCount = rowHeader.getChildCount();
        View time = rowHeader.findViewById(com.android.internal.R.id.time);
        boolean hasVisibleText = false;
        for (int i = 0; i < childCount; i++) {
            View child = rowHeader.getChildAt(i);
            if (child instanceof TextView
                    && child.getVisibility() != View.GONE
                    && !mDividers.contains(child.getId())
                    && child != time) {
                hasVisibleText = true;
                break;
            }
        }
        // in case no view is visible we make sure the time is visible
        int timeVisibility = !hasVisibleText
                || showsTime(row)
                ? View.VISIBLE : View.GONE;
        time.setVisibility(timeVisibility);
        View left = null;
        View right;
        for (int i = 0; i < childCount; i++) {
            View child = rowHeader.getChildAt(i);
            if (mDividers.contains(child.getId())) {
                boolean visible = false;
                // Lets find the item to the right
                for (i++; i < childCount; i++) {
                    right = rowHeader.getChildAt(i);
                    if (mDividers.contains(right.getId())) {
                        // A divider was found, this needs to be hidden
                        i--;
                        break;
                    } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
                        visible = left != null;
                        left = right;
                        break;
                    }
                }
                child.setVisibility(visible ? View.VISIBLE : View.GONE);
            } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
                left = child;
            }
        }
    }

    @VisibleForTesting
    boolean showsTime(ExpandableNotificationRow row) {
        StatusBarNotification sbn;
        if (NotificationBundleUi.isEnabled()) {
            sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null;
        } else {
            sbn = row.getEntryLegacy().getSbn();
        }
        return (sbn != null && sbn.getNotification().showsTime());
    }

    /**
     * Reset the modifications to this row for removing it from the group.
     */
    public void restoreChildNotification(ExpandableNotificationRow row) {
        if (row.getNotificationViewWrapper() == null) {
            return;
        }
        for (int compI = 0; compI < mProcessors.size(); compI++) {
            mProcessors.get(compI).apply(row, true /* reset */);
        }
        sanitizeTopLineViews(row);
    }

    private static class Processor {
        private final int mId;
        private final DataExtractor mExtractor;
        private final ViewComparator mComparator;
        private final ResultApplicator mApplicator;
        private final ExpandableNotificationRow mParentRow;
        private boolean mApply;
        private View mParentView;
        private Object mParentData;

        public static Processor forTextView(ExpandableNotificationRow row, int id) {
            return new Processor(row, id, null, TEXT_VIEW_COMPARATOR, VISIBILITY_APPLICATOR);
        }

        Processor(ExpandableNotificationRow row, int id, DataExtractor extractor,
                ViewComparator comparator,
                ResultApplicator applicator) {
            mId = id;
            mExtractor = extractor;
            mApplicator = applicator;
            mComparator = comparator;
            mParentRow = row;
        }

        public void init() {
            NotificationViewWrapper wrapper = mParentRow.getNotificationViewWrapper();
            View header = wrapper == null ? null : wrapper.getNotificationHeader();
            mParentView = header == null ? null : header.findViewById(mId);
            mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
            mApply = !mComparator.isEmpty(mParentView);
        }

        public void compareToGroupParent(ExpandableNotificationRow row) {
            if (!mApply) {
                return;
            }
            View contractedChild = row.getPrivateLayout().getContractedChild();
            if (contractedChild == null) {
                return;
            }
            View ownView = contractedChild.findViewById(mId);
            if (ownView == null) {
                // No view found. We still consider this to be the same to avoid weird flickering
                // when for example showing an undo notification
                return;
            }
            Object childData = mExtractor == null ? null : mExtractor.extractData(row);
            mApply = mComparator.compare(mParentView, ownView,
                    mParentData, childData);
        }

        public void apply(ExpandableNotificationRow row) {
            apply(row, false /* reset */);
        }

        public void apply(ExpandableNotificationRow row, boolean reset) {
            boolean apply = mApply && !reset;
            if (row.isSummaryWithChildren()) {
                applyToView(apply, reset, row.getNotificationViewWrapper().getNotificationHeader());
                return;
            }
            applyToView(apply, reset, row.getPrivateLayout().getContractedChild());
            applyToView(apply, reset, row.getPrivateLayout().getHeadsUpChild());
            applyToView(apply, reset, row.getPrivateLayout().getExpandedChild());
        }

        private void applyToView(boolean apply, boolean reset, View parent) {
            if (parent != null) {
                View view = parent.findViewById(mId);
                if (view != null && !mComparator.isEmpty(view)) {
                    mApplicator.apply(parent, view, apply, reset);
                }
            }
        }
    }

    private interface ViewComparator {
        /**
         * @param parent     the view with the given id in the group header
         * @param child      the view with the given id in the child notification
         * @param parentData optional data for the parent
         * @param childData  optional data for the child
         * @return whether to views are the same
         */
        boolean compare(View parent, View child, Object parentData, Object childData);

        boolean isEmpty(View view);
    }

    @VisibleForTesting
    interface DataExtractor {
        Object extractData(ExpandableNotificationRow row);
    }

    private static class BadgeComparator implements ViewComparator {
        @Override
        public boolean compare(View parent, View child, Object parentData, Object childData) {
            return parent.getVisibility() != View.GONE;
        }

        @Override
        public boolean isEmpty(View view) {
            if (AsyncGroupHeaderViewInflation.isEnabled() && view == null) {
                return true;
            }
            if (view instanceof ImageView) {
                return ((ImageView) view).getDrawable() == null;
            }
            return false;
        }
    }

    private static class TextViewComparator implements ViewComparator {
        @Override
        public boolean compare(View parent, View child, Object parentData, Object childData) {
            TextView parentView = (TextView) parent;
            CharSequence parentText = parentView == null ? "" : parentView.getText();
            TextView childView = (TextView) child;
            CharSequence childText = childView == null ? "" : childView.getText();
            return Objects.equals(parentText, childText);
        }

        @Override
        public boolean isEmpty(View view) {
            return view == null || TextUtils.isEmpty(((TextView) view).getText());
        }
    }

    @VisibleForTesting
    static class IconComparator implements ViewComparator {
        @Override
        public boolean compare(View parent, View child, Object parentData, Object childData) {
            return false;
        }

        protected boolean hasSameIcon(Object parentData, Object childData) {
            if (parentData == null || childData == null) {
                return false;
            }
            Icon parentIcon = ((Notification) parentData).getSmallIcon();
            Icon childIcon = ((Notification) childData).getSmallIcon();
            return parentIcon.sameAs(childIcon);
        }

        /**
         * @return whether two ImageViews have the same colorFilterSet or none at all
         */
        protected boolean hasSameColor(Object parentData, Object childData) {
            if ((parentData == null && childData != null)
                    || (parentData != null && childData == null)) {
                return false;
            }
            int parentColor = ((Notification) parentData).color;
            int childColor = ((Notification) childData).color;
            return parentColor == childColor;
        }

        @Override
        public boolean isEmpty(View view) {
            return false;
        }
    }

    private interface ResultApplicator {
        /**
         * @param parent the root view of the child notification
         * @param view   the view with the given id in the child notification
         * @param apply  whether the state should be applied or removed
         * @param reset  if [de]application is the result of a reset
         */
        void apply(View parent, View view, boolean apply, boolean reset);
    }

    private static class VisibilityApplicator implements ResultApplicator {

        @Override
        public void apply(View parent, View view, boolean apply, boolean reset) {
            if (view != null) {
                view.setVisibility(apply ? View.GONE : View.VISIBLE);
            }
        }
    }

    private static class AppNameApplicator extends VisibilityApplicator {

        @Override
        public void apply(View parent, View view, boolean apply, boolean reset) {
            if (!notificationsRedesignTemplates()
                    && reset && parent instanceof ConversationLayout) {
                ConversationLayout layout = (ConversationLayout) parent;
                apply = layout.shouldHideAppName();
            }
            super.apply(parent, view, apply, reset);
        }
    }

    private static class AppNameComparator extends TextViewComparator {
        @Override
        public boolean compare(View parent, View child, Object parentData, Object childData) {
            if (isEmpty(child)) {
                // In headerless notifications the AppName view exists but is usually GONE (and not
                // populated).  We need to treat this case as equal to the header in order to
                // deduplicate the view.
                return true;
            }
            return super.compare(parent, child, parentData, childData);
        }
    }

    private static class LeftIconApplicator implements ResultApplicator {

        public static final int[] MARGIN_ADJUSTED_VIEWS = {
                R.id.text,
                R.id.big_text,
                R.id.title,
                R.id.notification_main_column,
                R.id.notification_header,
                notificationsRedesignTemplates() ? R.id.notification_top_line : -1
        };

        @Override
        public void apply(View parent, View child, boolean apply, boolean reset) {
            ImageView leftIcon = child.findViewById(com.android.internal.R.id.left_icon);
            if (leftIcon == null) {
                return;
            }
            ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon);
            boolean keepRightIcon = rightIcon != null && Integer.valueOf(1).equals(
                    rightIcon.getTag(R.id.tag_keep_when_showing_left_icon));
            boolean leftIconUsesRightIconDrawable = Integer.valueOf(1).equals(
                    leftIcon.getTag(R.id.tag_uses_right_icon_drawable));
            if (leftIconUsesRightIconDrawable) {
                // Use the right drawable when showing the left, unless the right is being kept
                Drawable rightDrawable = rightIcon == null ? null : rightIcon.getDrawable();
                leftIcon.setImageDrawable(apply && !keepRightIcon ? rightDrawable : null);
            }
            boolean shouldShowLeftIcon = notificationsRedesignTemplates()
                            ? apply && leftIcon.getDrawable() != null
                            : apply;
            leftIcon.setVisibility(shouldShowLeftIcon ? View.VISIBLE : View.GONE);

            // update the right icon as well
            if (rightIcon != null) {
                boolean showRightIcon = (keepRightIcon || !apply)
                        && rightIcon.getDrawable() != null;
                rightIcon.setVisibility(showRightIcon ? View.VISIBLE : View.GONE);
                for (int viewId : MARGIN_ADJUSTED_VIEWS) {
                    adjustMargins(showRightIcon, child.findViewById(viewId));
                }
            }
        }

        void adjustMargins(boolean iconVisible, View target) {
            if (target == null) {
                return;
            }
            if (target instanceof ImageFloatingTextView) {
                ((ImageFloatingTextView) target).setHasImage(iconVisible);
                return;
            }
            final Integer data = (Integer) target.getTag(iconVisible
                    ? com.android.internal.R.id.tag_margin_end_when_icon_visible
                    : com.android.internal.R.id.tag_margin_end_when_icon_gone);
            if (data == null) {
                return;
            }
            final DisplayMetrics metrics = target.getResources().getDisplayMetrics();
            final int value = TypedValue.complexToDimensionPixelOffset(data, metrics);
            if (target instanceof NotificationHeaderView) {
                ((NotificationHeaderView) target).setTopLineExtraMarginEnd(value);
            } else {
                ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
                if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
                    ((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(value);
                    target.setLayoutParams(layoutParams);
                }
            }
        }
    }
}
