/*
 * Copyright (C) 2022 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.shade;

import android.content.ComponentCallbacks2;
import android.os.Looper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewTreeObserver;
import android.view.WindowManagerGlobal;

import com.android.systemui.DejankUtils;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.window.StatusBarWindowController;

import dagger.Lazy;

import java.util.concurrent.Executor;

import javax.inject.Inject;

/** An implementation of {@link ShadeController}. */
@SysUISingleton
public final class ShadeControllerImpl extends BaseShadeControllerImpl {

    private static final String TAG = "ShadeControllerImpl";
    private static final boolean SPEW = false;

    private final int mDisplayId;

    private final CommandQueue mCommandQueue;
    private final Executor mMainExecutor;
    private final WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor;
    private final KeyguardStateController mKeyguardStateController;
    private final NotificationShadeWindowController mNotificationShadeWindowController;
    private final StatusBarStateController mStatusBarStateController;
    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
    private final StatusBarWindowController mStatusBarWindowController;
    private final DeviceProvisionedController mDeviceProvisionedController;

    private final Lazy<NotificationPanelViewController> mNpvc;
    private final Lazy<AssistManager> mAssistManagerLazy;
    private final Lazy<NotificationGutsManager> mGutsManager;

    private boolean mExpandedVisible;
    private boolean mLockscreenOrShadeVisible;

    private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
    private ShadeVisibilityListener mShadeVisibilityListener;

    @Inject
    public ShadeControllerImpl(
            CommandQueue commandQueue,
            @Main Executor mainExecutor,
            WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor,
            KeyguardStateController keyguardStateController,
            StatusBarStateController statusBarStateController,
            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
            StatusBarWindowController statusBarWindowController,
            DeviceProvisionedController deviceProvisionedController,
            NotificationShadeWindowController notificationShadeWindowController,
            @DisplayId int displayId,
            Lazy<NotificationPanelViewController> shadeViewControllerLazy,
            Lazy<AssistManager> assistManagerLazy,
            Lazy<NotificationGutsManager> gutsManager
    ) {
        super(commandQueue,
                statusBarKeyguardViewManager,
                notificationShadeWindowController,
                assistManagerLazy);
        SceneContainerFlag.assertInLegacyMode();
        mCommandQueue = commandQueue;
        mMainExecutor = mainExecutor;
        mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor;
        mNpvc = shadeViewControllerLazy;
        mStatusBarStateController = statusBarStateController;
        mStatusBarWindowController = statusBarWindowController;
        mDeviceProvisionedController = deviceProvisionedController;
        mGutsManager = gutsManager;
        mNotificationShadeWindowController = notificationShadeWindowController;
        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
        mDisplayId = displayId;
        mKeyguardStateController = keyguardStateController;
        mAssistManagerLazy = assistManagerLazy;
    }

    @Override
    public boolean isShadeEnabled() {
        return mCommandQueue.panelsEnabled() && mDeviceProvisionedController.isCurrentUserSetup();
    }

    @Override
    public void instantExpandShade() {
        // Make our window larger and the panel expanded.
        makeExpandedVisible(true /* force */);
        getNpvc().expand(false /* animate */);
        getCommandQueue().recomputeDisableFlags(mDisplayId, false /* animate */);
    }

    @Override
    public void animateCollapseShade(int flags, boolean force, boolean delayed,
            float speedUpFactor) {
        int statusBarState = mStatusBarStateController.getState();
        if (!force && statusBarState != StatusBarState.SHADE
                && statusBarState != StatusBarState.SHADE_LOCKED) {
            runPostCollapseActions();
            return;
        }
        if (getNotificationShadeWindowView() != null
                && getNpvc().canBeCollapsed()
                && (flags & CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) == 0) {
            // release focus immediately to kick off focus change transition
            mNotificationShadeWindowController.setNotificationShadeFocusable(false);

            mNotificationShadeWindowViewController.cancelExpandHelper();
            getNpvc().collapse(true, delayed, speedUpFactor);
        }
    }

    @Override
    public void collapseWithDuration(int animationDuration) {
        mNpvc.get().collapseWithDuration(animationDuration);
    }

    @Override
    protected void expandToNotifications() {
        getNpvc().expandToNotifications();
    }

    @Override
    protected void expandToQs() {
        getNpvc().expandToQs();
    }

    @Override
    public boolean closeShadeIfOpen() {
        if (!getNpvc().isFullyCollapsed()) {
            getCommandQueue().animateCollapsePanels(
                    CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
            notifyVisibilityChanged(false);
            mAssistManagerLazy.get().hideAssist();
        }
        return false;
    }

    @Override
    public boolean isShadeFullyOpen() {
        return getNpvc().isShadeFullyExpanded();
    }

    @Override
    public boolean isExpandingOrCollapsing() {
        return getNpvc().isExpandingOrCollapsing();
    }
    @Override
    public void postAnimateCollapseShade() {
        mMainExecutor.execute(this::animateCollapseShade);
    }

    @Override
    public void postAnimateForceCollapseShade() {
        mMainExecutor.execute(this::animateCollapseShadeForced);
    }

    @Override
    public void postAnimateExpandQs() {
        mMainExecutor.execute(this::animateExpandQs);
    }

    @Override
    public void postOnShadeExpanded(Runnable executable) {
        getNpvc().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        if (getNotificationShadeWindowView().isVisibleToUser()) {
                            getNpvc().removeOnGlobalLayoutListener(this);
                            getNpvc().postToView(executable);
                        }
                    }
                });
    }

    @Override
    public void collapseShade() {
        collapseShadeInternal();
    }

    private boolean collapseShadeInternal() {
        if (!getNpvc().isFullyCollapsed()) {
            // close the shade if it was open
            animateCollapseShadeForcedDelayed();
            notifyVisibilityChanged(false);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void collapseShade(boolean animate) {
        if (animate) {
            boolean willCollapse = collapseShadeInternal();
            if (!willCollapse) {
                runPostCollapseActions();
            }
        } else if (!getNotifPresenter().isPresenterFullyCollapsed()) {
            instantCollapseShade();
            notifyVisibilityChanged(false);
        } else {
            runPostCollapseActions();
        }
    }

    @Override
    public void cancelExpansionAndCollapseShade() {
        if (getNpvc().isTracking()) {
            mNotificationShadeWindowViewController.cancelCurrentTouch();
        }
        if (getNpvc().isPanelExpanded()
                && mStatusBarStateController.getState() == StatusBarState.SHADE) {
            animateCollapseShade();
        }
    }

    @Override
    public void collapseOnMainThread() {
        if (Looper.getMainLooper().isCurrentThread()) {
            collapseShade();
        } else {
            mMainExecutor.execute(this::collapseShade);
        }
    }

    @Override
    public void onStatusBarTouch(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (mExpandedVisible) {
                animateCollapseShade();
            }
        }
    }

    @Override
    public void performHapticFeedback(int constant) {
        getNpvc().performHapticFeedback(constant);
    }

    @Override
    public void instantCollapseShade() {
        getNpvc().instantCollapse();
        runPostCollapseActions();
    }

    @Override
    public void makeExpandedVisible(boolean force) {
        if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible);
        if (!force && (mExpandedVisible || !getCommandQueue().panelsEnabled())) {
            return;
        }

        mExpandedVisible = true;

        // Expand the window to encompass the full screen in anticipation of the drag.
        // It's only possible to do atomically because the status bar is at the top of the screen!
        mNotificationShadeWindowController.setPanelVisible(true);

        notifyVisibilityChanged(true);
        getCommandQueue().recomputeDisableFlags(mDisplayId, !force /* animate */);
        notifyExpandedVisibleChanged(true);
    }

    @Override
    public void makeExpandedInvisible() {
        if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible);

        if (!mExpandedVisible || getNotificationShadeWindowView() == null) {
            return;
        }

        // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
        getNpvc().collapse(false, false, 1.0f);

        mExpandedVisible = false;
        notifyVisibilityChanged(false);

        // Update the visibility of notification shade and status bar window.
        mNotificationShadeWindowController.setPanelVisible(false);
        mStatusBarWindowController.setForceStatusBarVisible(false);

        // Close any guts that might be visible
        mGutsManager.get().closeAndSaveGuts(
                true /* removeLeavebehind */,
                true /* force */,
                true /* removeControls */,
                -1 /* x */,
                -1 /* y */,
                true /* resetMenu */);

        runPostCollapseActions();
        notifyExpandedVisibleChanged(false);
        getCommandQueue().recomputeDisableFlags(
                mDisplayId,
                getNpvc().shouldHideStatusBarIconsWhenExpanded());

        // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in
        // the bouncer appear animation.
        if (!mKeyguardStateController.isShowing()) {
            WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
        }
    }

    @Override
    public boolean isExpandedVisible() {
        return mExpandedVisible;
    }

    @Override
    public void setVisibilityListener(ShadeVisibilityListener listener) {
        mShadeVisibilityListener = listener;
    }

    private void notifyVisibilityChanged(boolean visible) {
        mWindowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(visible);
        if (mLockscreenOrShadeVisible != visible) {
            mLockscreenOrShadeVisible = visible;
            if (visible) {
                // It would be best if this could be done as a side effect of listening to the
                // [WindowRootViewVisibilityInteractor.isLockscreenOrShadeVisible] flow inside
                // NotificationShadeWindowViewController. However, there's no guarantee that the
                // flow will emit in the same frame as when the visibility changed, and we want the
                // DejankUtils to be notified immediately, so we do it immediately here.
                DejankUtils.notifyRendererOfExpensiveFrame(
                        getNotificationShadeWindowView(), "onShadeVisibilityChanged");
            }
        }
    }

    private void notifyExpandedVisibleChanged(boolean expandedVisible) {
        mShadeVisibilityListener.expandedVisibleChanged(expandedVisible);
    }

    @Override
    public void setNotificationShadeWindowViewController(
            NotificationShadeWindowViewController controller) {
        mNotificationShadeWindowViewController = controller;
    }

    private NotificationShadeWindowView getNotificationShadeWindowView() {
        return mNotificationShadeWindowViewController.getView();
    }

    private NotificationPanelViewController getNpvc() {
        return mNpvc.get();
    }

    @Override
    public void start() {
        getNpvc().setTrackingStartedListener(this::runPostCollapseActions);
        getNpvc().setOpenCloseListener(
                new OpenCloseListener() {
                    @Override
                    public void onClosingFinished() {
                        ShadeControllerImpl.this.onClosingFinished();
                    }

                    @Override
                    public void onOpenStarted() {
                        makeExpandedVisible(false);
                    }
                });
    }

    @Override
    public void collapseShadeForActivityStart() {
        if (isExpandedVisible() && !mStatusBarKeyguardViewManager.isBouncerShowing()) {
            animateCollapseShadeForcedDelayed();
        } else {
            // Do it after DismissAction has been processed to conserve the
            // needed ordering.
            mMainExecutor.execute(this::runPostCollapseActions);
        }
    }

}