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

import android.app.ActivityManager;
import android.car.Car;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;

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

import com.android.car.notification.CarNotificationListener;
import com.android.car.notification.CarNotificationView;
import com.android.car.notification.CarUxRestrictionManagerWrapper;
import com.android.car.notification.NotificationClickHandlerFactory;
import com.android.car.notification.NotificationClickHandlerFactory.OnNotificationClickListener;
import com.android.car.notification.NotificationDataManager;
import com.android.car.notification.NotificationViewController;
import com.android.car.notification.PreprocessingManager;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R;
import com.android.systemui.car.CarDeviceProvisionedController;
import com.android.systemui.car.CarServiceProvider;
import com.android.systemui.car.CarServiceProvider.CarServiceOnConnectedListener;
import com.android.systemui.car.window.OverlayPanelViewController;
import com.android.systemui.car.window.OverlayViewController;
import com.android.systemui.car.window.OverlayViewGlobalStateController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
import com.android.wm.shell.animation.FlingAnimationUtils;

import java.util.concurrent.Executor;

import javax.inject.Inject;

/** View controller for the notification panel. */
@SysUISingleton
public class NotificationPanelViewController extends OverlayPanelViewController
        implements CommandQueue.Callbacks {

    private static final boolean DEBUG = true;
    private static final String TAG = "NotificationPanelViewController";

    private final Context mContext;
    private final Resources mResources;
    private final CarServiceProvider mCarServiceProvider;
    private final IStatusBarService mBarService;
    private final CommandQueue mCommandQueue;
    private final Executor mUiBgExecutor;
    private final NotificationDataManager mNotificationDataManager;
    private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper;
    private final CarNotificationListener mCarNotificationListener;
    private final NotificationClickHandlerFactory mNotificationClickHandlerFactory;
    private final StatusBarStateController mStatusBarStateController;
    private final boolean mEnableHeadsUpNotificationWhenNotificationPanelOpen;
    private final NotificationVisibilityLogger mNotificationVisibilityLogger;

    private final boolean mFitTopSystemBarInset;
    private final boolean mFitBottomSystemBarInset;
    private final boolean mFitLeftSystemBarInset;
    private final boolean mFitRightSystemBarInset;

    private float mInitialBackgroundAlpha;
    private float mBackgroundAlphaDiff;

    private CarNotificationView mNotificationView;
    private RecyclerView mNotificationList;
    private NotificationViewController mNotificationViewController;

    private boolean mNotificationListAtEnd;
    private float mFirstTouchDownOnGlassPane;
    private boolean mNotificationListAtEndAtTimeOfTouch;
    private boolean mIsSwipingVerticallyToClose;
    private boolean mIsNotificationCardSwiping;
    private boolean mImeVisible = false;
    private boolean mOnConnectListenerAdded;

    private OnUnseenCountUpdateListener mUnseenCountUpdateListener;
    private OnNotificationClickListener mOnNotificationClickListener =
            (launchResult, alertEntry) -> {
                if (launchResult == ActivityManager.START_TASK_TO_FRONT
                        || launchResult == ActivityManager.START_SUCCESS
                        || launchResult == ActivityManager.START_DELIVERED_TO_TOP) {
                    animateCollapsePanel();
                }
            };

    private CarServiceOnConnectedListener mCarConnectedListener =
            new CarServiceOnConnectedListener() {
                @Override
                public void onConnected(Car car) {
                    CarUxRestrictionsManager carUxRestrictionsManager =
                            (CarUxRestrictionsManager)
                                    car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
                    mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
                            carUxRestrictionsManager);

                    PreprocessingManager preprocessingManager =
                            PreprocessingManager.getInstance(mContext);
                    preprocessingManager.setCarUxRestrictionManagerWrapper(
                            mCarUxRestrictionManagerWrapper);

                    mNotificationViewController.enable();
                }
            };

    @Inject
    public NotificationPanelViewController(
            Context context,
            @Main Resources resources,
            OverlayViewGlobalStateController overlayViewGlobalStateController,
            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
            @UiBackground Executor uiBgExecutor,

            /* Other things */
            CarServiceProvider carServiceProvider,
            CarDeviceProvisionedController carDeviceProvisionedController,

            /* Things needed for notifications */
            IStatusBarService barService,
            CommandQueue commandQueue,
            NotificationDataManager notificationDataManager,
            CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
            CarNotificationListener carNotificationListener,
            NotificationClickHandlerFactory notificationClickHandlerFactory,
            NotificationVisibilityLogger notificationVisibilityLogger,

            /* Things that need to be replaced */
            StatusBarStateController statusBarStateController
    ) {
        super(context, resources, R.id.notification_panel_stub, overlayViewGlobalStateController,
                flingAnimationUtilsBuilder, carDeviceProvisionedController);
        mContext = context;
        mResources = resources;
        mCarServiceProvider = carServiceProvider;
        mBarService = barService;
        mCommandQueue = commandQueue;
        mUiBgExecutor = uiBgExecutor;
        mNotificationDataManager = notificationDataManager;
        mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper;
        mCarNotificationListener = carNotificationListener;
        mNotificationClickHandlerFactory = notificationClickHandlerFactory;
        mStatusBarStateController = statusBarStateController;
        mNotificationVisibilityLogger = notificationVisibilityLogger;

        mCommandQueue.addCallback(this);

        // Notification background setup.
        mInitialBackgroundAlpha = (float) mResources.getInteger(
                R.integer.config_initialNotificationBackgroundAlpha) / 100;
        if (mInitialBackgroundAlpha < 0 || mInitialBackgroundAlpha > 100) {
            throw new RuntimeException(
                    "Unable to setup notification bar due to incorrect initial background alpha"
                            + " percentage");
        }
        float finalBackgroundAlpha = Math.max(
                mInitialBackgroundAlpha,
                (float) mResources.getInteger(
                        R.integer.config_finalNotificationBackgroundAlpha) / 100);
        if (finalBackgroundAlpha < 0 || finalBackgroundAlpha > 100) {
            throw new RuntimeException(
                    "Unable to setup notification bar due to incorrect final background alpha"
                            + " percentage");
        }
        mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha;

        mEnableHeadsUpNotificationWhenNotificationPanelOpen = mResources.getBoolean(
                com.android.car.notification.R.bool
                        .config_enableHeadsUpNotificationWhenNotificationPanelOpen);

        mFitTopSystemBarInset = mResources.getBoolean(
                R.bool.config_notif_panel_inset_by_top_systembar);
        mFitBottomSystemBarInset = mResources.getBoolean(
                R.bool.config_notif_panel_inset_by_bottom_systembar);
        mFitLeftSystemBarInset = mResources.getBoolean(
                R.bool.config_notif_panel_inset_by_left_systembar);
        mFitRightSystemBarInset = mResources.getBoolean(
                R.bool.config_notif_panel_inset_by_right_systembar);

        // Inflate view on instantiation to properly initialize listeners even if panel has
        // not been opened.
        getOverlayViewGlobalStateController().inflateView(this);
    }

    // CommandQueue.Callbacks

    @Override
    public void animateExpandNotificationsPanel() {
        if (!isPanelExpanded()) {
            toggle();
        }
    }

    @Override
    public void animateCollapsePanels(int flags, boolean force) {
        if (isPanelExpanded()) {
            toggle();
        }
    }

    @Override
    public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
            boolean showImeSwitcher) {
        if (mContext.getDisplayId() != displayId) {
            return;
        }
        mImeVisible = (vis & InputMethodService.IME_VISIBLE) != 0;
    }

    // OverlayViewController

    @Override
    protected void onFinishInflate() {
        reinflate();
    }

    @Override
    protected void hideInternal() {
        super.hideInternal();
        mNotificationVisibilityLogger.stop();
    }

    @Override
    protected int getFocusAreaViewId() {
        return R.id.notification_container;
    }

    @Override
    protected boolean shouldShowNavigationBarInsets() {
        return true;
    }

    @Override
    protected boolean shouldShowStatusBarInsets() {
        return true;
    }

    @Override
    protected int getInsetSidesToFit() {
        int insetSidesToFit = OverlayViewController.NO_INSET_SIDE;

        if (mFitTopSystemBarInset) {
            insetSidesToFit = insetSidesToFit | WindowInsets.Side.TOP;
        }

        if (mFitBottomSystemBarInset) {
            insetSidesToFit = insetSidesToFit | WindowInsets.Side.BOTTOM;
        }

        if (mFitLeftSystemBarInset) {
            insetSidesToFit = insetSidesToFit | WindowInsets.Side.LEFT;
        }

        if (mFitRightSystemBarInset) {
            insetSidesToFit = insetSidesToFit | WindowInsets.Side.RIGHT;
        }

        return insetSidesToFit;
    }

    @Override
    protected boolean shouldShowHUN() {
        return mEnableHeadsUpNotificationWhenNotificationPanelOpen;
    }

    @Override
    protected boolean shouldUseStableInsets() {
        // When IME is visible, then the inset from the nav bar should not be applied.
        return !mImeVisible;
    }

    /** Reinflates the view. */
    public void reinflate() {
        // Do not reinflate the view if it has not been inflated at all.
        if (!isInflated()) return;

        mNotificationClickHandlerFactory.unregisterClickListener(mOnNotificationClickListener);

        if (mOnConnectListenerAdded) {
            mCarServiceProvider.removeListener(mCarConnectedListener);
            mOnConnectListenerAdded = false;
        }

        ViewGroup container = (ViewGroup) getLayout();
        container.removeView(mNotificationView);

        mNotificationView = (CarNotificationView) LayoutInflater.from(mContext).inflate(
                R.layout.notification_center_activity, container,
                /* attachToRoot= */ false);
        mNotificationView.setKeyEventHandler(
                event -> {
                    if (event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
                        return false;
                    }

                    if (event.getAction() == KeyEvent.ACTION_UP && isPanelExpanded()) {
                        toggle();
                    }
                    return true;
                });

        container.addView(mNotificationView);
        onNotificationViewInflated();
    }

    private void onNotificationViewInflated() {
        // Find views.
        mNotificationView = getLayout().findViewById(R.id.notification_view);
        setUpHandleBar();
        setupNotificationPanel();

        mNotificationClickHandlerFactory.registerClickListener(mOnNotificationClickListener);

        mNotificationDataManager.setOnUnseenCountUpdateListener(() -> {
            if (mUnseenCountUpdateListener != null) {
                // Don't show unseen markers for <= LOW importance notifications to be consistent
                // with how these notifications are handled on phones
                int unseenCount =
                        mNotificationDataManager.getNonLowImportanceUnseenNotificationCount(
                                mCarNotificationListener.getCurrentRanking());
                mUnseenCountUpdateListener.onUnseenCountUpdate(unseenCount);
            }
            if (isPanelExpanded()) {
                // only report the seen notifications when the panel is expanded
                mCarNotificationListener.setNotificationsShown(
                        mNotificationDataManager.getSeenNotifications());
            }
            // This logs both when the notification panel is expanded and when the notification
            // panel is scrolled.
            mNotificationVisibilityLogger.log(isPanelExpanded());
        });

        mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory);
        mNotificationViewController = new NotificationViewController(
                mNotificationView,
                PreprocessingManager.getInstance(mContext),
                mCarNotificationListener,
                mCarUxRestrictionManagerWrapper);

        if (!mOnConnectListenerAdded) {
            mCarServiceProvider.addListener(mCarConnectedListener);
            mOnConnectListenerAdded = true;
        }
    }

    private void setupNotificationPanel() {
        View glassPane = mNotificationView.findViewById(R.id.glass_pane);
        mNotificationList = mNotificationView.findViewById(R.id.notifications);
        GestureDetector closeGestureDetector = new GestureDetector(mContext,
                new CloseGestureListener() {
                    @Override
                    protected void close() {
                        if (isPanelExpanded()) {
                            animateCollapsePanel();
                        }
                    }
                });

        // The glass pane is used to view touch events before passed to the notification list.
        // This allows us to initialize gesture listeners and detect when to close the notifications
        glassPane.setOnTouchListener((v, event) -> {
            if (isClosingAction(event)) {
                mNotificationListAtEndAtTimeOfTouch = false;
            }
            if (isOpeningAction(event)) {
                mFirstTouchDownOnGlassPane = event.getRawX();
                mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd;
                // Reset the tracker when there is a touch down on the glass pane.
                setIsTracking(false);
                // Pass the down event to gesture detector so that it knows where the touch event
                // started.
                closeGestureDetector.onTouchEvent(event);
            }
            return false;
        });

        mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                // Check if we can scroll vertically in the animation direction.
                if (!mNotificationList.canScrollVertically(mAnimateDirection)) {
                    mNotificationListAtEnd = true;
                    return;
                }
                mNotificationListAtEnd = false;
                mIsSwipingVerticallyToClose = false;
                mNotificationListAtEndAtTimeOfTouch = false;
            }
        });

        mNotificationList.setOnTouchListener((v, event) -> {
            mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
                    > SWIPE_MAX_OFF_PATH;
            if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) {
                // We need to save the state here as if notification card is swiping we will
                // change the mNotificationListAtEndAtTimeOfTouch. This is to protect
                // closing the notification shade while the notification card is being swiped.
                mIsSwipingVerticallyToClose = true;
            }

            // If the card is swiping we should not allow the notification shade to close.
            // Hence setting mNotificationListAtEndAtTimeOfTouch to false will stop that
            // for us. We are also checking for isTracking() because while swiping the
            // notification shade to close if the user goes a bit horizontal while swiping
            // upwards then also this should close.
            if (mIsNotificationCardSwiping && !isTracking()) {
                mNotificationListAtEndAtTimeOfTouch = false;
            }

            boolean handled = closeGestureDetector.onTouchEvent(event);
            boolean isTracking = isTracking();
            Rect rect = getLayout().getClipBounds();
            float clippedHeight = 0;
            if (rect != null) {
                clippedHeight = rect.bottom;
            }
            if (!handled && isClosingAction(event) && mIsSwipingVerticallyToClose) {
                if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) {
                    animatePanel(DEFAULT_FLING_VELOCITY, false);
                } else if (clippedHeight != getLayout().getHeight() && isTracking) {
                    // this can be caused when user is at the end of the list and trying to
                    // fling to top of the list by scrolling down.
                    animatePanel(DEFAULT_FLING_VELOCITY, true);
                }
            }

            // Updating the mNotificationListAtEndAtTimeOfTouch state has to be done after
            // the event has been passed to the closeGestureDetector above, such that the
            // closeGestureDetector sees the up event before the state has changed.
            if (isClosingAction(event)) {
                mNotificationListAtEndAtTimeOfTouch = false;
            }
            return handled || isTracking;
        });
    }

    /** Called when the car power state is changed to ON. */
    public void onCarPowerStateOn() {
        if (mNotificationClickHandlerFactory != null) {
            mNotificationClickHandlerFactory.clearAllNotifications(mContext);
        }
        mNotificationDataManager.clearAll();
    }

    /**
     * Forwards the call to clear all Notification cache.
     * Note: This is a blocking call so should not execute any long-running or time-consuming tasks
     * like storing cache.
     */
    public void clearCache() {
        mCarNotificationListener.clearCache();
    }

    // OverlayPanelViewController

    @Override
    protected boolean shouldAnimateCollapsePanel() {
        return true;
    }

    @Override
    protected void onAnimateCollapsePanel() {
        // no-op
    }

    @Override
    protected boolean shouldAnimateExpandPanel() {
        return mCommandQueue.panelsEnabled();
    }

    @Override
    protected void onAnimateExpandPanel() {
        mNotificationList.scrollToPosition(0);
    }

    @Override
    protected int getSettleClosePercentage() {
        return mResources.getInteger(R.integer.notification_settle_close_percentage);
    }

    @Override
    protected void onCollapseAnimationEnd() {
        mNotificationViewController.onVisibilityChanged(false);
    }

    @Override
    protected void onExpandAnimationEnd() {
        mNotificationView.setVisibleNotificationsAsSeen();
        mNotificationViewController.onVisibilityChanged(true);
    }

    @Override
    protected void onPanelVisible(boolean visible) {
        super.onPanelVisible(visible);
        mUiBgExecutor.execute(() -> {
            try {
                if (visible) {
                    // When notification panel is open even just a bit, we want to clear
                    // notification effects.
                    boolean clearNotificationEffects =
                            mStatusBarStateController.getState() != StatusBarState.KEYGUARD;
                    mBarService.onPanelRevealed(clearNotificationEffects,
                            mNotificationDataManager.getVisibleNotifications().size());
                } else {
                    mBarService.onPanelHidden();
                }
            } catch (RemoteException ex) {
                // Won't fail unless the world has ended.
                Log.e(TAG, String.format(
                        "Unable to notify StatusBarService of panel visibility: %s", visible));
            }
        });

    }

    @Override
    protected void onPanelExpanded(boolean expand) {
        super.onPanelExpanded(expand);

        if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
            if (DEBUG) {
                Log.v(TAG, "clearing notification effects from setExpandedHeight");
            }
            clearNotificationEffects();
        }
        if (!expand) {
            mNotificationVisibilityLogger.log(isPanelExpanded());
        }
    }

    /**
     * Clear Buzz/Beep/Blink.
     */
    private void clearNotificationEffects() {
        try {
            mBarService.clearNotificationEffects();
        } catch (RemoteException e) {
            // Won't fail unless the world has ended.
        }
    }

    @Override
    protected void onOpenScrollStart() {
        mNotificationList.scrollToPosition(0);
    }

    @Override
    protected void onScroll(int y) {
        super.onScroll(y);

        if (mNotificationView.getHeight() > 0) {
            Drawable background = mNotificationView.getBackground().mutate();
            background.setAlpha((int) (getBackgroundAlpha(y) * 255));
            mNotificationView.setBackground(background);
        }
    }

    @Override
    protected boolean shouldAllowClosingScroll() {
        // Unless the notification list is at the end, the panel shouldn't be allowed to
        // collapse on scroll.
        return mNotificationListAtEndAtTimeOfTouch;
    }

    @Override
    protected Integer getHandleBarViewId() {
        return R.id.handle_bar;
    }

    /**
     * Calculates the alpha value for the background based on how much of the notification
     * shade is visible to the user. When the notification shade is completely open then
     * alpha value will be 1.
     */
    private float getBackgroundAlpha(int y) {
        float fractionCovered =
                ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y))
                        / mNotificationView.getHeight();
        return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff;
    }

    /** Sets the unseen count listener. */
    public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) {
        mUnseenCountUpdateListener = listener;
    }

    /** Listener that is updated when the number of unseen notifications changes. */
    public interface OnUnseenCountUpdateListener {
        /**
         * This method is automatically called whenever there is an update to the number of unseen
         * notifications. This method can be extended by OEMs to customize the desired logic.
         */
        void onUnseenCountUpdate(int unseenNotificationCount);
    }
}
