/*
 * Copyright 2021 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.navigationbar;

import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;

import static com.android.systemui.navigationbar.NavBarHelper.transitionMode;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IMMERSIVE_MODE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;

import android.app.StatusBarManager;
import android.app.StatusBarManager.WindowVisibleState;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import com.android.internal.statusbar.LetterboxDetails;
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.AutoHideUiElement;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.AutoHideController;
import com.android.systemui.statusbar.phone.BarTransitions;
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.phone.LightBarTransitionsController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.pip.Pip;

import java.io.PrintWriter;
import java.util.Optional;
import java.util.function.Consumer;

import javax.inject.Inject;

/** */
@SysUISingleton
public class TaskbarDelegate implements CommandQueue.Callbacks,
        OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener,
        Dumpable {
    private static final String TAG = TaskbarDelegate.class.getSimpleName();

    private EdgeBackGestureHandler mEdgeBackGestureHandler;
    private final LightBarTransitionsController.Factory mLightBarTransitionsControllerFactory;
    private boolean mInitialized;
    private CommandQueue mCommandQueue;
    private OverviewProxyService mOverviewProxyService;
    private NavBarHelper mNavBarHelper;
    private NavigationModeController mNavigationModeController;
    private SysUiState mSysUiState;
    private AutoHideController mAutoHideController;
    private LightBarController mLightBarController;
    private LightBarTransitionsController mLightBarTransitionsController;
    private TaskStackChangeListeners mTaskStackChangeListeners;
    private Optional<Pip> mPipOptional;
    private int mDisplayId;
    private int mNavigationIconHints;
    private final NavBarHelper.NavbarTaskbarStateUpdater mNavbarTaskbarStateUpdater =
            new NavBarHelper.NavbarTaskbarStateUpdater() {
                @Override
                public void updateAccessibilityServicesState() {
                    updateSysuiFlags();
                }

                @Override
                public void updateAssistantAvailable(boolean available,
                        boolean longPressHomeEnabled) {
                    updateAssistantAvailability(available, longPressHomeEnabled);
                }
            };
    private int mDisabledFlags;
    private @WindowVisibleState int mTaskBarWindowState = WINDOW_STATE_SHOWING;

    private @TransitionMode int mTransitionMode;
    private @Appearance int mAppearance;
    private @Behavior int mBehavior;
    private final Context mContext;
    private final DisplayManager mDisplayManager;
    private Context mWindowContext;
    private ScreenPinningNotify mScreenPinningNotify;
    private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
        @Override
        public void onLockTaskModeChanged(int mode) {
            mSysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, mode == LOCK_TASK_MODE_PINNED)
                    .commitUpdate(mDisplayId);
        }
    };

    private int mNavigationMode = -1;
    private final Consumer<Rect> mPipListener;

    /**
     * Tracks the system calls for when taskbar should transiently show or hide so we can return
     * this value in {@link AutoHideUiElement#isVisible()} below.
     *
     * This also gets set by {@link #onTaskbarAutohideSuspend(boolean)} to force show the transient
     * taskbar if launcher has requested to suspend auto-hide behavior.
     */
    private boolean mTaskbarTransientShowing;
    private final AutoHideUiElement mAutoHideUiElement = new AutoHideUiElement() {
        @Override
        public void synchronizeState() {
        }

        @Override
        public boolean isVisible() {
            return mTaskbarTransientShowing;
        }

        @Override
        public void hide() {
            clearTransient();
        }
    };

    private BackAnimation mBackAnimation;

    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
    @Inject
    public TaskbarDelegate(Context context,
            LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
        mLightBarTransitionsControllerFactory = lightBarTransitionsControllerFactory;

        mContext = context;
        mDisplayManager = mContext.getSystemService(DisplayManager.class);
        mPipListener = (bounds) -> {
            mEdgeBackGestureHandler.setPipStashExclusionBounds(bounds);
        };
        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
        mStatusBarKeyguardViewManager.setTaskbarDelegate(this);
    }

    public void setDependencies(CommandQueue commandQueue,
            OverviewProxyService overviewProxyService,
            NavBarHelper navBarHelper,
            NavigationModeController navigationModeController,
            SysUiState sysUiState, DumpManager dumpManager,
            AutoHideController autoHideController,
            LightBarController lightBarController,
            Optional<Pip> pipOptional,
            BackAnimation backAnimation,
            TaskStackChangeListeners taskStackChangeListeners) {
        // TODO: adding this in the ctor results in a dagger dependency cycle :(
        mCommandQueue = commandQueue;
        mOverviewProxyService = overviewProxyService;
        mNavBarHelper = navBarHelper;
        mNavigationModeController = navigationModeController;
        mSysUiState = sysUiState;
        dumpManager.registerDumpable(this);
        mAutoHideController = autoHideController;
        mLightBarController = lightBarController;
        mPipOptional = pipOptional;
        mBackAnimation = backAnimation;
        mLightBarTransitionsController = createLightBarTransitionsController();
        mTaskStackChangeListeners = taskStackChangeListeners;
        mEdgeBackGestureHandler = navBarHelper.getEdgeBackGestureHandler();
    }

    // Separated into a method to keep setDependencies() clean/readable.
    private LightBarTransitionsController createLightBarTransitionsController() {

        LightBarTransitionsController controller =  mLightBarTransitionsControllerFactory.create(
                new LightBarTransitionsController.DarkIntensityApplier() {
                    @Override
                    public void applyDarkIntensity(float darkIntensity) {
                        mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
                    }

                    @Override
                    public int getTintAnimationDuration() {
                        return LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION;
                    }
                });

        return controller;
    }

    public void init(int displayId) {
        if (mInitialized) {
            return;
        }
        mDisplayId = displayId;
        parseCurrentSysuiState();
        mCommandQueue.addCallback(this);
        mOverviewProxyService.addCallback(this);
        onNavigationModeChanged(mNavigationModeController.addListener(this));
        mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
        // Initialize component callback
        Display display = mDisplayManager.getDisplay(displayId);
        mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
        mScreenPinningNotify = new ScreenPinningNotify(mWindowContext);
        // Set initial state for any listeners
        updateSysuiFlags();
        mAutoHideController.setNavigationBar(mAutoHideUiElement);
        mLightBarController.setNavigationBar(mLightBarTransitionsController);
        mPipOptional.ifPresent(this::addPipExclusionBoundsChangeListener);
        mEdgeBackGestureHandler.setBackAnimation(mBackAnimation);
        mEdgeBackGestureHandler.onConfigurationChanged(mContext.getResources().getConfiguration());
        mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
        mInitialized = true;
    }

    public void destroy() {
        if (!mInitialized) {
            return;
        }
        mCommandQueue.removeCallback(this);
        mOverviewProxyService.removeCallback(this);
        mNavigationModeController.removeListener(this);
        mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
        mScreenPinningNotify = null;
        mWindowContext = null;
        mAutoHideController.setNavigationBar(null);
        mLightBarTransitionsController.destroy();
        mLightBarController.setNavigationBar(null);
        mPipOptional.ifPresent(this::removePipExclusionBoundsChangeListener);
        mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener);
        mInitialized = false;
    }

    void addPipExclusionBoundsChangeListener(Pip pip) {
        pip.addPipExclusionBoundsChangeListener(mPipListener);
    }

    void removePipExclusionBoundsChangeListener(Pip pip) {
        pip.removePipExclusionBoundsChangeListener(mPipListener);
    }

    /**
     * Returns {@code true} if this taskBar is {@link #init(int)}.
     * Returns {@code false} if this taskbar has not yet been {@link #init(int)}
     * or has been {@link #destroy()}.
     */
    public boolean isInitialized() {
        return mInitialized;
    }

    private void parseCurrentSysuiState() {
        NavBarHelper.CurrentSysuiState state = mNavBarHelper.getCurrentSysuiState();
        if (state.mWindowStateDisplayId == mDisplayId) {
            mTaskBarWindowState = state.mWindowState;
        }
    }

    private void updateSysuiFlags() {
        int a11yFlags = mNavBarHelper.getA11yButtonState();
        boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
        boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;

        mSysUiState.setFlag(SYSUI_STATE_A11Y_BUTTON_CLICKABLE, clickable)
                .setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable)
                .setFlag(SYSUI_STATE_IME_SHOWING,
                        (mNavigationIconHints & NAVIGATION_HINT_BACK_ALT) != 0)
                .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING,
                        (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0)
                .setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
                        (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0)
                .setFlag(SYSUI_STATE_HOME_DISABLED,
                        (mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0)
                .setFlag(SYSUI_STATE_BACK_DISABLED,
                        (mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)
                .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible())
                .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
                        allowSystemGestureIgnoringBarVisibility())
                .setFlag(SYSUI_STATE_IMMERSIVE_MODE, isImmersiveMode())
                .commitUpdate(mDisplayId);
    }

    boolean isOverviewEnabled() {
        return (mSysUiState.getFlags() & View.STATUS_BAR_DISABLE_RECENT) == 0;
    }

    private void updateAssistantAvailability(boolean assistantAvailable,
            boolean longPressHomeEnabled) {
        if (mOverviewProxyService.getProxy() == null) {
            return;
        }

        try {
            mOverviewProxyService.getProxy().onAssistantAvailable(assistantAvailable,
                    longPressHomeEnabled);
        } catch (RemoteException e) {
            Log.e(TAG, "onAssistantAvailable() failed, available: " + assistantAvailable, e);
        }
    }

    @Override
    public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
            boolean showImeSwitcher) {
        boolean imeShown = mNavBarHelper.isImeShown(vis);
        if (!imeShown) {
            // Count imperceptible changes as visible so we transition taskbar out quickly.
            imeShown = (vis & InputMethodService.IME_VISIBLE_IMPERCEPTIBLE) != 0;
        }
        showImeSwitcher = imeShown && showImeSwitcher;
        int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition,
                imeShown, showImeSwitcher);
        if (hints != mNavigationIconHints) {
            mNavigationIconHints = hints;
            updateSysuiFlags();
        }
    }

    @Override
    public void setWindowState(int displayId, int window, int state) {
        if (displayId == mDisplayId
                && window == StatusBarManager.WINDOW_NAVIGATION_BAR
                && mTaskBarWindowState != state) {
            mTaskBarWindowState = state;
            updateSysuiFlags();
        }
    }

    @Override
    public void onRotationProposal(int rotation, boolean isValid) {
        mOverviewProxyService.onRotationProposal(rotation, isValid);
    }

    @Override
    public void disable(int displayId, int state1, int state2, boolean animate) {
        mDisabledFlags = state1;
        updateSysuiFlags();
        mOverviewProxyService.disable(displayId, state1, state2, animate);
    }

    @Override
    public void onSystemBarAttributesChanged(int displayId, int appearance,
            AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, int behavior,
            @InsetsType int requestedVisibleTypes, String packageName,
            LetterboxDetails[] letterboxDetails) {
        mOverviewProxyService.onSystemBarAttributesChanged(displayId, behavior);
        boolean nbModeChanged = false;
        if (mAppearance != appearance) {
            mAppearance = appearance;
            nbModeChanged = updateTransitionMode(
                    transitionMode(mTaskbarTransientShowing, appearance));
        }
        if (displayId == mDisplayId) {
            mLightBarController.onNavigationBarAppearanceChanged(appearance, nbModeChanged,
                    BarTransitions.MODE_TRANSPARENT, navbarColorManagedByIme);
        }
        if (mBehavior != behavior) {
            mBehavior = behavior;
            updateSysuiFlags();
        }
    }

    @Override
    public void showTransient(int displayId, @InsetsType int types, boolean isGestureOnSystemBar) {
        if (displayId != mDisplayId) {
            return;
        }
        if ((types & WindowInsets.Type.navigationBars()) == 0) {
            return;
        }
        if (!mTaskbarTransientShowing) {
            mTaskbarTransientShowing = true;
            onTransientStateChanged();
        }
    }

    @Override
    public void abortTransient(int displayId, @InsetsType int types) {
        if (displayId != mDisplayId) {
            return;
        }
        if ((types & WindowInsets.Type.navigationBars()) == 0) {
            return;
        }
        clearTransient();
    }

    @Override
    public void onTaskbarAutohideSuspend(boolean suspend) {
        if (suspend) {
            mAutoHideController.suspendAutoHide();
        } else {
            mAutoHideController.resumeSuspendedAutoHide();
        }
    }

    @Override
    public void toggleTaskbar() {
        if (mOverviewProxyService.getProxy() == null) {
            return;
        }

        try {
            mOverviewProxyService.getProxy().onTaskbarToggled();
        } catch (RemoteException e) {
            Log.e(TAG, "onTaskbarToggled() failed", e);
        }
    }

    private void clearTransient() {
        if (mTaskbarTransientShowing) {
            mTaskbarTransientShowing = false;
            onTransientStateChanged();
        }
    }

    private void onTransientStateChanged() {
        mEdgeBackGestureHandler.onNavBarTransientStateChanged(mTaskbarTransientShowing);

        final int transitionMode = transitionMode(mTaskbarTransientShowing, mAppearance);
        if (updateTransitionMode(transitionMode)) {
            mLightBarController.onNavigationBarModeChanged(transitionMode);
        }
    }

    private boolean updateTransitionMode(int barMode) {
        if (mTransitionMode != barMode) {
            mTransitionMode = barMode;
            if (mAutoHideController != null) {
                mAutoHideController.touchAutoHide();
            }
            return true;
        }
        return false;
    }

    @Override
    public void onRecentsAnimationStateChanged(boolean running) {
    }

    @Override
    public void onNavigationModeChanged(int mode) {
        mNavigationMode = mode;
        mEdgeBackGestureHandler.onNavigationModeChanged(mode);
    }

    private boolean isWindowVisible() {
        return mTaskBarWindowState == WINDOW_STATE_SHOWING;
    }

    private boolean allowSystemGestureIgnoringBarVisibility() {
        return mBehavior != BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
    }

    private boolean isImmersiveMode() {
        return mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
    }

    public void onConfigurationChanged(Configuration configuration) {
        mEdgeBackGestureHandler.onConfigurationChanged(configuration);
    }

    @Override
    public void showPinningEnterExitToast(boolean entering) {
        updateSysuiFlags();
        if (mScreenPinningNotify == null) {
            return;
        }
        if (entering) {
            mScreenPinningNotify.showPinningStartToast();
        } else {
            mScreenPinningNotify.showPinningExitToast();
        }
    }

    @Override
    public void showPinningEscapeToast() {
        updateSysuiFlags();
        if (mScreenPinningNotify == null) {
            return;
        }
        mScreenPinningNotify.showEscapeToast(QuickStepContract.isGesturalMode(mNavigationMode),
                !QuickStepContract.isGesturalMode(mNavigationMode));
    }

    @VisibleForTesting
    int getNavigationMode() {
        return mNavigationMode;
    }

    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
        pw.println("TaskbarDelegate (displayId=" + mDisplayId + "):");
        pw.println("  mNavigationIconHints=" + mNavigationIconHints);
        pw.println("  mNavigationMode=" + mNavigationMode);
        pw.println("  mDisabledFlags=" + mDisabledFlags);
        pw.println("  mTaskBarWindowState=" + mTaskBarWindowState);
        pw.println("  mBehavior=" + mBehavior);
        pw.println("  mTaskbarTransientShowing=" + mTaskbarTransientShowing);
        mEdgeBackGestureHandler.dump(pw);
    }
}
