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

import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
import static com.android.wm.shell.Flags.enableTaskbarOnPhones;

import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.IWindowManager;
import android.view.View;
import android.view.WindowManagerGlobal;
import android.window.DesktopExperienceFlags;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.app.displaylib.DisplayDecorationListener;
import com.android.app.displaylib.DisplaysWithDecorationsRepositoryCompat;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.RegisterStatusBarResult;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.systemui.Dumpable;
import com.android.systemui.LauncherProxyService;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.display.flags.WmCallbackForSysDecorFlag;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.views.NavigationBar;
import com.android.systemui.navigationbar.views.NavigationBarView;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.AutoHideControllerStore;
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.util.Utils;
import com.android.systemui.util.settings.SecureSettings;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.pip.Pip;

import dalvik.annotation.optimization.NeverCompile;

import kotlinx.coroutines.CoroutineDispatcher;

import java.io.PrintWriter;
import java.util.Optional;
import java.util.concurrent.Executor;

import javax.inject.Inject;

@SysUISingleton
public class NavigationBarControllerImpl implements
        ConfigurationController.ConfigurationListener,
        NavigationModeController.ModeChangedListener,
        Dumpable, NavigationBarController {

    private static final String TAG = NavigationBarControllerImpl.class.getSimpleName();

    private final Context mContext;
    private final Executor mExecutor;
    private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
    private final SecureSettings mSecureSettings;
    private final DisplayTracker mDisplayTracker;
    private final DisplayManager mDisplayManager;
    private final TaskbarDelegate mTaskbarDelegate;
    private final NavBarHelper mNavBarHelper;
    private int mNavMode;
    /**
     * Indicates whether the active display is a large screen, e.g. tablets, foldable devices in
     * the unfolded state.
     */
    @VisibleForTesting boolean mIsLargeScreen;
    /**
     * Indicates whether the device is a phone, rather than everything else (e.g. foldables,
     * tablets) is considered not a handheld device.
     */
    @VisibleForTesting boolean mIsPhone;

    /** A displayId - nav bar maps. */
    @VisibleForTesting
    SparseArray<NavigationBar> mNavigationBars = new SparseArray<>();

    /** Local cache for {@link IWindowManager#hasNavigationBar(int)}. */
    private SparseBooleanArray mHasNavBarOrTaskbar = new SparseBooleanArray();

    // Tracks config changes that will actually recreate the nav bar
    private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
            ActivityInfo.CONFIG_FONT_SCALE
                    | ActivityInfo.CONFIG_UI_MODE);

    @Inject
    public NavigationBarControllerImpl(Context context,
            LauncherProxyService launcherProxyService,
            NavigationModeController navigationModeController,
            SysUiState sysUiFlagsContainer,
            CommandQueue commandQueue,
            @Main Executor mainExecutor,
            ConfigurationController configurationController,
            NavBarHelper navBarHelper,
            TaskbarDelegate taskbarDelegate,
            NavigationBarComponent.Factory navigationBarComponentFactory,
            DumpManager dumpManager,
            AutoHideControllerStore autoHideControllerStore,
            LightBarController lightBarController,
            TaskStackChangeListeners taskStackChangeListeners,
            Optional<Pip> pipOptional,
            Optional<BackAnimation> backAnimation,
            SecureSettings secureSettings,
            DisplayTracker displayTracker,
            DeviceStateManager deviceStateManager,
            DisplaysWithDecorationsRepositoryCompat displaysWithDecorationsRepositoryCompat,
            @Main CoroutineDispatcher mainCoroutineDispatcher) {
        mContext = context;
        mExecutor = mainExecutor;
        mNavigationBarComponentFactory = navigationBarComponentFactory;
        mSecureSettings = secureSettings;
        mDisplayTracker = displayTracker;
        mDisplayManager = mContext.getSystemService(DisplayManager.class);
        commandQueue.addCallback(mCommandQueueCallbacks);
        if (WmCallbackForSysDecorFlag.isEnabled()) {
            displaysWithDecorationsRepositoryCompat.registerDisplayDecorationListener(
                    mDisplayDecorationListener, mainCoroutineDispatcher);
        }
        configurationController.addCallback(this);
        mConfigChanges.applyNewConfig(mContext.getResources());
        mNavMode = navigationModeController.addListener(this);
        mNavBarHelper = navBarHelper;
        mTaskbarDelegate = taskbarDelegate;
        mTaskbarDelegate.setDependencies(commandQueue, launcherProxyService,
                navBarHelper, navigationModeController, sysUiFlagsContainer,
                dumpManager, autoHideControllerStore.forDisplay(mContext.getDisplayId()),
                lightBarController, pipOptional, backAnimation.orElse(null),
                taskStackChangeListeners, displayTracker);
        mIsLargeScreen = isLargeScreen(mContext);
        mIsPhone = determineIfPhone(mContext, deviceStateManager);
        dumpManager.registerDumpable(this);
    }

    private boolean determineIfPhone(Context context, DeviceStateManager deviceStateManager) {
        if (android.hardware.devicestate.feature.flags.Flags.deviceStatePropertyMigration()) {
            return !Utils.isDeviceFoldable(context.getResources(), deviceStateManager);
        } else {
            return context.getResources().getIntArray(R.array.config_foldedDeviceStates).length
                    == 0;
        }
    }

    @Override
    public void onConfigChanged(Configuration newConfig) {
        boolean isOldConfigLargeScreen = mIsLargeScreen;
        mIsLargeScreen = isLargeScreen(mContext);
        boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources());
        boolean largeScreenChanged = mIsLargeScreen != isOldConfigLargeScreen;
        // TODO(b/332635834): Disable this logging once b/332635834 is fixed.
        Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
                + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized()
                + " willApplyConfigToNavbars=" + willApplyConfig
                + " navBarCount=" + mNavigationBars.size());
        // If we folded/unfolded while in 3 button, show navbar in folded state, hide in unfolded
        if (largeScreenChanged && updateNavbarForTaskbar()) {
            return;
        }

        if (willApplyConfig) {
            for (int i = 0; i < mNavigationBars.size(); i++) {
                recreateNavigationBar(mNavigationBars.keyAt(i));
            }
        } else {
            for (int i = 0; i < mNavigationBars.size(); i++) {
                mNavigationBars.valueAt(i).onConfigurationChanged(newConfig);
            }
        }
    }

    @Override
    public void onNavigationModeChanged(int mode) {
        if (mNavMode == mode) {
            return;
        }
        final int oldMode = mNavMode;
        mNavMode = mode;

        mExecutor.execute(() -> {
            // create/destroy nav bar based on nav mode only in unfolded state
            if (oldMode != mNavMode) {
                updateNavbarForTaskbar();
            }
            for (int i = 0; i < mNavigationBars.size(); i++) {
                NavigationBar navBar = mNavigationBars.valueAt(i);
                if (navBar == null) {
                    continue;
                }
                navBar.getView().updateStates();
            }
        });
    }

    /**
     * Returns the cached value from {@link #mHasNavBarOrTaskbar} if it exists, otherwise calls
     * {@link #updateHasNavBarForDisplay(int)} to update the cache and returns that value.
     */
    @Override
    public boolean canCreateNavBarOrTaskBar(int displayId) {
        if (mHasNavBarOrTaskbar.indexOfKey(displayId) > -1) {
            return mHasNavBarOrTaskbar.get(displayId);
        }

        return updateHasNavBarForDisplay(displayId);
    }

    /**
     * Updates the cache {@link #mHasNavBarOrTaskbar} for the given displayId by querying
     * {@link IWindowManager#hasNavigationBar(int)}.
     */
    private boolean updateHasNavBarForDisplay(int displayId) {
        final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();

        try {
            boolean hasNavigationBarOrTaskbar = wms.hasNavigationBar(displayId);
            mHasNavBarOrTaskbar.put(displayId, hasNavigationBarOrTaskbar);
            return hasNavigationBarOrTaskbar;
        } catch (RemoteException e) {
            // Cannot get wms, just return false with warning message.
            Log.w(TAG, "Cannot get WindowManager.", e);
            return false;
        }
    }

    /** @see #initializeTaskbarIfNecessary() */
    private boolean updateNavbarForTaskbar() {
        boolean taskbarShown = initializeTaskbarIfNecessary();
        if (!taskbarShown && mNavigationBars.get(mContext.getDisplayId()) == null) {
            createNavigationBar(mContext.getDisplay(), null, null);
        }
        return taskbarShown;
    }

    /** @return {@code true} if taskbar is enabled, false otherwise */
    private boolean initializeTaskbarIfNecessary() {
        final int displayId = mContext.getDisplayId();
        final boolean canCreateNavBarOrTaskbar = canCreateNavBarOrTaskBar(displayId);
        boolean taskbarEnabled = supportsTaskbar() && canCreateNavBarOrTaskbar;

        if (taskbarEnabled) {
            Trace.beginSection("NavigationBarController#initializeTaskbarIfNecessary");
            // Hint to NavBarHelper if we are replacing an existing bar to skip extra work
            mNavBarHelper.setTogglingNavbarTaskbar(mNavigationBars.contains(displayId));
            // Remove navigation bar when taskbar is showing
            removeNavigationBar(displayId);
            mTaskbarDelegate.init(displayId);
            mNavBarHelper.setTogglingNavbarTaskbar(false);
            Trace.endSection();

        } else {
            mTaskbarDelegate.destroy();
        }
        return taskbarEnabled;
    }

    @VisibleForTesting
    boolean supportsTaskbar() {
        // Enable for tablets, unfolded state on a foldable device, (non handheld AND flag is set),
        // or handheld when enableTaskbarOnPhones() returns true.
        boolean foldedOrPhone = !mIsPhone || enableTaskbarOnPhones();
        return mIsLargeScreen || foldedOrPhone;
    }

    // TODO: b/408503553 - Remove system decor callbacks once the flag is cleaned up.
    private final CommandQueue.Callbacks mCommandQueueCallbacks = new CommandQueue.Callbacks() {
        @Override
        public void onDisplayRemoved(int displayId) {
            mDisplayDecorationListener.onDisplayRemoveSystemDecorations(displayId);
        }

        @Override
        public void onDisplayRemoveSystemDecorations(int displayId) {
            WmCallbackForSysDecorFlag.assertInLegacyMode();
            mDisplayDecorationListener.onDisplayRemoveSystemDecorations(displayId);
        }

        @Override
        public void onDisplayAddSystemDecorations(int displayId) {
            WmCallbackForSysDecorFlag.assertInLegacyMode();
            mDisplayDecorationListener.onDisplayAddSystemDecorations(displayId);
        }

        @Override
        public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
            final NavigationBar navigationBar = getNavigationBar(displayId);
            if (navigationBar != null) {
                navigationBar.setNavigationBarLumaSamplingEnabled(enable);
            }
        }

        @Override
        public void showPinningEnterExitToast(boolean entering) {
            int displayId = mContext.getDisplayId();
            final NavigationBarView navBarView = getNavigationBarView(displayId);
            if (navBarView != null) {
                navBarView.showPinningEnterExitToast(entering);
            }
        }

        @Override
        public void showPinningEscapeToast() {
            int displayId = mContext.getDisplayId();
            final NavigationBarView navBarView = getNavigationBarView(displayId);
            if (navBarView != null) {
                navBarView.showPinningEscapeToast();
            }
        }
    };

    private final DisplayDecorationListener mDisplayDecorationListener =
            new DisplayDecorationListener() {
                @Override
                public void onDisplayRemoved(int displayId) {
                    onDisplayRemoveSystemDecorations(displayId);
                }

                @Override
                public void onDisplayRemoveSystemDecorations(int displayId) {
                    removeNavigationBar(displayId);
                    mHasNavBarOrTaskbar.delete(displayId);
                }

                @Override
                public void onDisplayAddSystemDecorations(int displayId) {
                    if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) {
                        updateHasNavBarForDisplay(displayId);
                    }
                    Display display = mDisplayManager.getDisplay(displayId);
                    mIsLargeScreen = isLargeScreen(mContext);
                    if (mNavigationBars.get(displayId) == null) {
                        createNavigationBar(display, null /* savedState */, null /* result */);
                    }
                }};

    /**
     * Recreates the navigation bar for the given display.
     */
    private void recreateNavigationBar(int displayId) {
        // TODO: Improve this flow so that we don't need to create a new nav bar but just
        //       the view
        Bundle savedState = new Bundle();
        NavigationBar bar = mNavigationBars.get(displayId);
        if (bar != null) {
            bar.onSaveInstanceState(savedState);
        }
        removeNavigationBar(displayId);
        createNavigationBar(mDisplayManager.getDisplay(displayId), savedState, null /* result */);
    }

    @Override
    public void createNavigationBars(final boolean includeDefaultDisplay,
            RegisterStatusBarResult result) {
        // Don't need to create nav bar on the default display if we initialize TaskBar.
        final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
                && !initializeTaskbarIfNecessary();
        Display[] displays = mDisplayTracker.getAllDisplays();
        for (Display display : displays) {
            if (shouldCreateDefaultNavbar
                    || display.getDisplayId() != mDisplayTracker.getDefaultDisplayId()) {
                createNavigationBar(display, null /* savedState */, result);
            }
        }
    }

    /**
     * Adds a navigation bar on default display or an external display if the display supports
     * system decorations.
     *
     * @param display the display to add navigation bar on.
     */
    @VisibleForTesting
    void createNavigationBar(Display display, Bundle savedState,
            RegisterStatusBarResult result) {
        if (display == null) {
            return;
        }

        final int displayId = display.getDisplayId();
        final boolean isOnDefaultDisplay = displayId == mDisplayTracker.getDefaultDisplayId();

        if (!canCreateNavBarOrTaskBar(displayId)) {
            return;
        }

        // Taskbar on connected displays will be created by TaskbarManager through display
        // decoration callback when flag is on.
        if (!isOnDefaultDisplay
                && DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
            return;
        }

        // We may show TaskBar on the default display for large screen device. Don't need to create
        // navigation bar for this case.
        if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) {
            return;
        }

        final Context context = isOnDefaultDisplay
                ? mContext
                : mContext.createDisplayContext(display);

        NavigationBarComponent component = mNavigationBarComponentFactory.create(
                context, savedState);
        NavigationBar navBar = component.getNavigationBar();
        navBar.init();
        mNavigationBars.put(displayId, navBar);

        navBar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {
                if (result != null) {
                    navBar.setImeWindowStatus(display.getDisplayId(), result.mImeWindowVis,
                            result.mImeBackDisposition, result.mShowImeSwitcher);
                }
            }

            @Override
            public void onViewDetachedFromWindow(View v) {
                v.removeOnAttachStateChangeListener(this);
            }
        });
    }

    @Override
    public void removeNavigationBar(int displayId) {
        NavigationBar navBar = mNavigationBars.get(displayId);
        if (navBar != null) {
            navBar.destroyView();
            mNavigationBars.remove(displayId);
        }
    }

    @Override
    public void checkNavBarModes(int displayId) {
        NavigationBar navBar = mNavigationBars.get(displayId);
        if (navBar != null) {
            navBar.checkNavBarModes();
        } else {
            mTaskbarDelegate.checkNavBarModes(displayId);
        }
    }

    @Override
    public void finishBarAnimations(int displayId) {
        NavigationBar navBar = mNavigationBars.get(displayId);
        if (navBar != null) {
            navBar.finishBarAnimations();
        } else {
            mTaskbarDelegate.finishBarAnimations(displayId);
        }
    }

    @Override
    public void touchAutoDim(int displayId) {
        NavigationBar navBar = mNavigationBars.get(displayId);
        if (navBar != null) {
            navBar.touchAutoDim();
        } else {
            mTaskbarDelegate.touchAutoDim(displayId);
        }
    }

    @Override
    public void transitionTo(int displayId, @TransitionMode int barMode, boolean animate) {
        NavigationBar navBar = mNavigationBars.get(displayId);
        if (navBar != null) {
            navBar.transitionTo(barMode, animate);
        } else {
            mTaskbarDelegate.transitionTo(displayId, barMode, animate);
        }
    }

    @Override
    public void disableAnimationsDuringHide(int displayId, long delay) {
        NavigationBar navBar = mNavigationBars.get(displayId);
        if (navBar != null) {
            navBar.disableAnimationsDuringHide(delay);
        }
    }

    @Override
    public @Nullable NavigationBarView getDefaultNavigationBarView() {
        return getNavigationBarView(mDisplayTracker.getDefaultDisplayId());
    }

    @Override
    public @Nullable NavigationBarView getNavigationBarView(int displayId) {
        NavigationBar navBar = getNavigationBar(displayId);
        return (navBar == null) ? null : navBar.getView();
    }

    @Override
    public @Nullable NavigationBar getNavigationBar(int displayId) {
        return mNavigationBars.get(displayId);
    }

    @Override
    public boolean isOverviewEnabled(int displayId) {
        final NavigationBarView navBarView = getNavigationBarView(displayId);
        if (navBarView != null) {
            return navBarView.isOverviewEnabled();
        } else {
            return mTaskbarDelegate.isOverviewEnabled();
        }
    }

    @Override
    @Nullable
    public NavigationBar getDefaultNavigationBar() {
        return mNavigationBars.get(mDisplayTracker.getDefaultDisplayId());
    }

    @NeverCompile
    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
        pw.println("mIsLargeScreen=" + mIsLargeScreen);
        pw.println("mNavMode=" + mNavMode);
        for (int i = 0; i < mNavigationBars.size(); i++) {
            if (i > 0) {
                pw.println();
            }
            mNavigationBars.valueAt(i).dump(pw);
        }
    }
}
