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

import android.annotation.CallSuper;
import android.annotation.ColorInt;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;

import androidx.annotation.VisibleForTesting;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;

import com.android.car.notification.AlertEntry;
import com.android.car.notification.NotificationClickHandlerFactory;
import com.android.car.notification.NotificationUtils;
import com.android.car.notification.R;

/**
 * The base view holder class that all template view holders should extend.
 */
public abstract class CarNotificationBaseViewHolder extends RecyclerView.ViewHolder {
    private final Context mContext;
    private final NotificationClickHandlerFactory mClickHandlerFactory;

    @Nullable
    private final CardView mCardView; // can be null for group child or group summary notification
    @Nullable
    private final View mInnerView; // can be null for GroupNotificationViewHolder
    @Nullable
    private final CarNotificationHeaderView mHeaderView;
    @Nullable
    private final CarNotificationBodyView mBodyView;
    @Nullable
    private final CarNotificationActionsView mActionsView;
    @Nullable
    private final ImageButton mDismissButton;
    private final float mIsSeenAlpha;
    private final boolean mUseCustomColorForWarningNotification;
    private final boolean mUseCustomColorForInformationNotification;

    /**
     * Focus change listener to make the dismiss button transparent or opaque depending on whether
     * the card view has focus.
     */
    private final ViewTreeObserver.OnGlobalFocusChangeListener mFocusChangeListener;

    /**
     * Whether to hide the dismiss button. If the bound {@link AlertEntry} is dismissible, a dismiss
     * button will normally be shown when card view has focus. If this field is true, no dismiss
     * button will be shown. This is the case for the group summary notification in a collapsed
     * group.
     */
    private boolean mHideDismissButton;
    private boolean mUseLauncherIcon;

    @ColorInt
    private final int mDefaultBackgroundColor;
    @ColorInt
    private final int mDefaultCarAccentColor;
    @ColorInt
    private final int mCustomInformationBackgroundColor;
    @ColorInt
    private final int mCustomInformationPrimaryColor;
    @ColorInt
    private final int mCustomInformationSecondaryColor;
    @ColorInt
    private final int mCustomWarningBackgroundColor;
    @ColorInt
    private final int mCustomWarningPrimaryColor;
    @ColorInt
    private final int mCustomWarningSecondaryColor;
    @ColorInt
    private int mDefaultPrimaryForegroundColor;
    @ColorInt
    private int mDefaultSecondaryForegroundColor;
    @ColorInt
    private int mCalculatedPrimaryForegroundColor;
    @ColorInt
    private int mCalculatedSecondaryForegroundColor;
    @ColorInt
    private int mSmallIconColor;
    @ColorInt
    private int mBackgroundColor;

    private AlertEntry mAlertEntry;
    private boolean mIsAnimating;
    private boolean mHasColor;
    private boolean mIsColorized;
    private boolean mEnableCardBackgroundColorForCategoryNavigation;
    private boolean mEnableCardBackgroundColorForSystemApp;
    private boolean mEnableSmallIconAccentColor;
    private boolean mAlwaysShowDismissButton;
    private boolean mIsSeen;

    /**
     * Tracks if the foreground colors have been calculated for the binding of the view holder.
     * The colors should only be calculated once per binding.
     **/
    private boolean mInitializedColors;

    CarNotificationBaseViewHolder(View itemView,
            NotificationClickHandlerFactory clickHandlerFactory) {
        super(itemView);
        mContext = itemView.getContext();
        mClickHandlerFactory = clickHandlerFactory;
        mCardView = itemView.findViewById(R.id.card_view);
        mInnerView = itemView.findViewById(R.id.inner_template_view);
        mHeaderView = itemView.findViewById(R.id.notification_header);
        mBodyView = itemView.findViewById(R.id.notification_body);
        mActionsView = itemView.findViewById(R.id.notification_actions);
        mDismissButton = itemView.findViewById(R.id.dismiss_button);
        mAlwaysShowDismissButton = mContext.getResources().getBoolean(
                R.bool.config_alwaysShowNotificationDismissButton);
        mUseLauncherIcon = mContext.getResources().getBoolean(R.bool.config_useLauncherIcon);
        mFocusChangeListener = (oldFocus, newFocus) -> {
            if (mDismissButton != null && !mAlwaysShowDismissButton) {
                // The dismiss button should only be visible when the focus is on this notification
                // or within it. Use alpha rather than visibility so that focus can move up to the
                // previous notification's dismiss button when action buttons are not present.
                mDismissButton.setImageAlpha(itemView.hasFocus() ? 255 : 0);
            }
        };
        mDefaultBackgroundColor = mContext.getColor(R.color.notification_background_color);
        mDefaultCarAccentColor = NotificationUtils.getAttrColor(mContext,
                android.R.attr.colorAccent);
        mDefaultPrimaryForegroundColor = mContext.getColor(R.color.primary_text_color);
        mDefaultSecondaryForegroundColor = mContext.getColor(R.color.secondary_text_color);
        mCustomInformationBackgroundColor = mContext.getColor(R.color.information_background_color);
        mCustomInformationPrimaryColor = mContext.getColor(R.color.information_primary_text_color);
        mCustomInformationSecondaryColor = mContext.getColor(
                R.color.information_secondary_text_color);
        mCustomWarningBackgroundColor = mContext.getColor(R.color.warning_background_color);
        mCustomWarningPrimaryColor = mContext.getColor(R.color.warning_primary_text_color);
        mCustomWarningSecondaryColor = mContext.getColor(R.color.warning_secondary_text_color);
        mEnableCardBackgroundColorForCategoryNavigation =
                mContext.getResources().getBoolean(
                        R.bool.config_enableCardBackgroundColorForCategoryNavigation);
        mEnableCardBackgroundColorForSystemApp =
                mContext.getResources().getBoolean(
                        R.bool.config_enableCardBackgroundColorForSystemApp);
        mEnableSmallIconAccentColor =
                mContext.getResources().getBoolean(R.bool.config_enableSmallIconAccentColor);
        mIsSeenAlpha = mContext.getResources().getFloat(R.dimen.config_olderNotificationsAlpha);
        mUseCustomColorForWarningNotification = mContext.getResources().getBoolean(
                R.color.warning_background_color);
        mUseCustomColorForInformationNotification = mContext.getResources().getBoolean(
                R.color.information_background_color);
    }

    /**
     * Binds a {@link AlertEntry} to a notification template. Base class sets the
     * clicking event for the card view and calls recycling methods.
     *
     * @param alertEntry the notification to be bound.
     * @param isInGroup whether this notification is part of a grouped notification.
     */
    @CallSuper
    public void bind(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp, boolean isSeen) {
        reset();
        mIsSeen = isSeen;
        mAlertEntry = alertEntry;

        if (isInGroup) {
            mInnerView.setBackgroundColor(mDefaultBackgroundColor);
            mInnerView.setOnClickListener(mClickHandlerFactory.getClickHandler(alertEntry));
        } else if (mCardView != null) {
            mCardView.setOnClickListener(mClickHandlerFactory.getClickHandler(alertEntry));
        }
        updateDismissButton(alertEntry, isHeadsUp);

        bindCardView(mCardView, isInGroup);
        bindHeader(mHeaderView, isInGroup);
        bindBody(mBodyView, isInGroup);

        itemView.setAlpha(mIsSeen ? mIsSeenAlpha : 1);
    }

    protected final Context getContext() {
        return mContext;
    }

    /**
     * Binds a {@link AlertEntry} to a notification template's card.
     *
     * @param cardView the CardView the notification should be bound to.
     * @param isInGroup whether this notification is part of a grouped notification.
     */
    void bindCardView(CardView cardView, boolean isInGroup) {
        initializeColors(isInGroup);

        if (cardView == null) {
            return;
        }

        if (canChangeCardBackgroundColor() && ((mHasColor && mIsColorized)
                || mUseCustomColorForInformationNotification
                || mUseCustomColorForWarningNotification) && !isInGroup) {
            cardView.setCardBackgroundColor(mBackgroundColor);
        }
    }

    /**
     * Binds a {@link AlertEntry} to a notification template's header.
     *
     * @param headerView the CarNotificationHeaderView the notification should be bound to.
     * @param isInGroup whether this notification is part of a grouped notification.
     */
    void bindHeader(CarNotificationHeaderView headerView, boolean isInGroup) {
        if (headerView == null) return;
        initializeColors(isInGroup);

        headerView.setSmallIconColor(mSmallIconColor);
        headerView.setHeaderTextColor(mCalculatedPrimaryForegroundColor);
    }

    /**
     * Binds a {@link AlertEntry} to a notification template's body.
     *
     * @param bodyView the CarNotificationBodyView the notification should be bound to.
     * @param isInGroup whether this notification is part of a grouped notification.
     */
    void bindBody(CarNotificationBodyView bodyView,
            boolean isInGroup) {
        if (bodyView == null) return;
        initializeColors(isInGroup);

        bodyView.setPrimaryTextColor(mCalculatedPrimaryForegroundColor);
        bodyView.setSecondaryTextColor(mCalculatedSecondaryForegroundColor);
        bodyView.setTimeTextColor(mCalculatedPrimaryForegroundColor);
    }

    private void initializeColors(boolean isInGroup) {
        if (mInitializedColors) return;
        Notification notification = getAlertEntry().getNotification();

        mHasColor = notification.color != Notification.COLOR_DEFAULT;
        mIsColorized = notification.extras.getBoolean(Notification.EXTRA_COLORIZED, false);

        mCalculatedPrimaryForegroundColor = mDefaultPrimaryForegroundColor;
        mCalculatedSecondaryForegroundColor = mDefaultSecondaryForegroundColor;
        if (canChangeCardBackgroundColor() && !isInGroup) {
            if (mHasColor && mIsColorized) {
                mBackgroundColor = notification.color;
            }
            boolean isWarningCategory = Notification.CATEGORY_CAR_WARNING.equals(
                    notification.category);
            boolean isInformationCategory = Notification.CATEGORY_CAR_INFORMATION.equals(
                    notification.category);
            if (mUseCustomColorForWarningNotification && isWarningCategory) {
                mBackgroundColor = mCustomWarningBackgroundColor;
                mDefaultPrimaryForegroundColor = mCustomWarningPrimaryColor;
                mDefaultSecondaryForegroundColor = mCustomWarningSecondaryColor;
            } else if (mUseCustomColorForInformationNotification && isInformationCategory) {
                mBackgroundColor = mCustomInformationBackgroundColor;
                mDefaultPrimaryForegroundColor = mCustomInformationPrimaryColor;
                mDefaultSecondaryForegroundColor = mCustomInformationSecondaryColor;
            }
            mCalculatedPrimaryForegroundColor = NotificationUtils.resolveContrastColor(
                    mDefaultPrimaryForegroundColor, mBackgroundColor);
            mCalculatedSecondaryForegroundColor = NotificationUtils.resolveContrastColor(
                    mDefaultSecondaryForegroundColor, mBackgroundColor);
        }
        mSmallIconColor =
                hasCustomBackgroundColor() ? mCalculatedPrimaryForegroundColor : getAccentColor();

        mInitializedColors = true;
    }


    private boolean canChangeCardBackgroundColor() {
        Notification notification = getAlertEntry().getNotification();

        boolean isSystemApp = mEnableCardBackgroundColorForSystemApp &&
                NotificationUtils.isSystemApp(mContext, getAlertEntry().getStatusBarNotification());
        boolean isSignedWithPlatformKey = NotificationUtils.isSignedWithPlatformKey(mContext,
                getAlertEntry().getStatusBarNotification());
        boolean isNavigationCategory = mEnableCardBackgroundColorForCategoryNavigation
                && Notification.CATEGORY_NAVIGATION.equals(notification.category);
        boolean isWarningCategory = mUseCustomColorForWarningNotification
                && Notification.CATEGORY_CAR_WARNING.equals(notification.category);
        boolean isInformationCategory = mUseCustomColorForInformationNotification
                && Notification.CATEGORY_CAR_INFORMATION.equals(notification.category);
        return isSystemApp || isNavigationCategory || isSignedWithPlatformKey || isWarningCategory
                || isInformationCategory;
    }

    /**
     * Returns the accent color for this notification.
     */
    @ColorInt
    int getAccentColor() {

        int color = getAlertEntry().getNotification().color;
        if (mEnableSmallIconAccentColor && color != Notification.COLOR_DEFAULT) {
            return color;
        }
        return mDefaultCarAccentColor;
    }

    /**
     * Returns whether this card has a custom background color.
     */
    boolean hasCustomBackgroundColor() {
        return mBackgroundColor != mDefaultBackgroundColor;
    }

    /**
     * Child view holders should override and call super to recycle any custom component
     * that's not handled by {@link CarNotificationHeaderView}, {@link CarNotificationBodyView} and
     * {@link CarNotificationActionsView}.
     * Note that any child class that is not calling {@link #bind} has to call this method directly.
     */
    @CallSuper
    void reset() {
        mAlertEntry = null;
        mBackgroundColor = mDefaultBackgroundColor;
        mInitializedColors = false;
        mIsSeen = false;

        itemView.setTranslationX(0);
        itemView.setAlpha(1f);

        if (mCardView != null) {
            mCardView.setOnClickListener(null);
            mCardView.setCardBackgroundColor(mDefaultBackgroundColor);
        }

        if (mBodyView != null) {
            mBodyView.reset();
        }

        if (mActionsView != null) {
            mActionsView.reset();
        }

        itemView.getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
        if (mDismissButton != null) {
            if (!mAlwaysShowDismissButton) {
                mDismissButton.setImageAlpha(0);
            }
            mDismissButton.setVisibility(View.GONE);
        }
    }

    /**
     * Returns the current {@link AlertEntry} that this view holder is holding.
     * Note that any child class that is not calling {@link #bind} has to override this method.
     */
    public AlertEntry getAlertEntry() {
        return mAlertEntry;
    }

    /**
     * Returns true if the panel notification contained in this view holder can be swiped away.
     */
    public boolean isDismissible() {
        if (mAlertEntry == null) {
            return true;
        }

        return (getAlertEntry().getNotification().flags
                & (Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_ONGOING_EVENT)) == 0;
    }

    void updateDismissButton(AlertEntry alertEntry, boolean isHeadsUp) {
        if (mDismissButton == null) {
            return;
        }
        // isDismissible only applies to panel notifications, not HUNs
        if ((!isHeadsUp && !isDismissible()) || mHideDismissButton) {
            hideDismissButton();
            return;
        }
        if (!mAlwaysShowDismissButton) {
            mDismissButton.setImageAlpha(0);
        }
        mDismissButton.setVisibility(View.VISIBLE);
        if (!isHeadsUp) {
            // Only set the click listener here for panel notifications - HUNs already have one
            // provided from the CarHeadsUpNotificationManager
            mDismissButton.setOnClickListener(getDismissHandler(alertEntry));
        }
        itemView.getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
    }

    void hideDismissButton() {
        if (mDismissButton == null) {
            return;
        }
        mDismissButton.setVisibility(View.GONE);
        itemView.getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
    }

    /**
     * Returns the TranslationX of the ItemView.
     */
    public float getSwipeTranslationX() {
        return itemView.getTranslationX();
    }

    /**
     * Sets the TranslationX of the ItemView.
     */
    public void setSwipeTranslationX(float translationX) {
        itemView.setTranslationX(translationX);
    }

    /**
     * Sets the alpha of the ItemView.
     */
    public void setSwipeAlpha(float alpha) {
        itemView.setAlpha(alpha);
    }

    /**
     * Sets whether this view holder has ongoing animation.
     */
    public void setIsAnimating(boolean animating) {
        mIsAnimating = animating;
    }

    /**
     * Returns true if this view holder has ongoing animation.
     */
    public boolean isAnimating() {
        return mIsAnimating;
    }

    /**
     * Returns is seen alpha if is seen is {@code true}.
     */
    public float getAlpha() {
        return mIsSeen ? mIsSeenAlpha : 1;
    }

    @VisibleForTesting
    public boolean shouldHideDismissButton() {
        return mHideDismissButton;
    }

    public void setHideDismissButton(boolean hideDismissButton) {
        mHideDismissButton = hideDismissButton;
    }

    View.OnClickListener getDismissHandler(AlertEntry alertEntry) {
        return mClickHandlerFactory.getDismissHandler(alertEntry);
    }
}
