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

import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;

import android.content.Context;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;

import androidx.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.WMComponent;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.notetask.NoteTaskInitializer;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.onehanded.OneHandedUiEventLogger;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.sysui.ShellInterface;

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

import javax.inject.Inject;

/**
 * A SystemUI service that starts with the SystemUI application and sets up any bindings between
 * Shell and SysUI components.  This service starts happens after the {@link WMComponent} has
 * already been initialized and may only reference Shell components that are explicitly exported to
 * SystemUI (see {@link WMComponent}.
 *
 * eg. SysUI application starts
 *     -> SystemUIFactory is initialized
 *       -> WMComponent is created
 *         -> WMShellBaseModule dependencies are injected
 *         -> WMShellModule (form-factory specific) dependencies are injected
 *       -> SysUIComponent is created
 *         -> WMComponents are explicitly provided to SysUIComponent for injection into SysUI code
 *     -> SysUI services are started
 *       -> WMShell starts and binds SysUI with Shell components via exported Shell interfaces
 */
@SysUISingleton
public final class WMShell implements
        CoreStartable,
        CommandQueue.Callbacks {
    private static final String TAG = WMShell.class.getName();
    private static final int INVALID_SYSUI_STATE_MASK =
            SYSUI_STATE_DIALOG_SHOWING
                    | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
                    | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
                    | SYSUI_STATE_BOUNCER_SHOWING
                    | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
                    | SYSUI_STATE_BUBBLES_EXPANDED
                    | SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
                    | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;

    private final Context mContext;
    // Shell interfaces
    private final ShellInterface mShell;
    private final Optional<Pip> mPipOptional;
    private final Optional<SplitScreen> mSplitScreenOptional;
    private final Optional<OneHanded> mOneHandedOptional;
    private final Optional<DesktopMode> mDesktopModeOptional;
    private final Optional<RecentTasks> mRecentTasksOptional;

    private final CommandQueue mCommandQueue;
    private final ConfigurationController mConfigurationController;
    private final KeyguardStateController mKeyguardStateController;
    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
    private final ScreenLifecycle mScreenLifecycle;
    private final SysUiState mSysUiState;
    private final WakefulnessLifecycle mWakefulnessLifecycle;
    private final UserTracker mUserTracker;
    private final DisplayTracker mDisplayTracker;
    private final NoteTaskInitializer mNoteTaskInitializer;
    private final Executor mSysUiMainExecutor;

    // Listeners and callbacks. Note that we prefer member variable over anonymous class here to
    // avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference
    // internally and anonymous class could be released after registration.
    private final ConfigurationController.ConfigurationListener mConfigurationListener =
            new ConfigurationController.ConfigurationListener() {
                @Override
                public void onConfigChanged(Configuration newConfig) {
                    mShell.onConfigurationChanged(newConfig);
                }
            };
    private final KeyguardStateController.Callback mKeyguardStateCallback =
            new KeyguardStateController.Callback() {
                @Override
                public void onKeyguardShowingChanged() {
                    mShell.onKeyguardVisibilityChanged(mKeyguardStateController.isShowing(),
                            mKeyguardStateController.isOccluded(),
                            mKeyguardStateController.isAnimatingBetweenKeyguardAndSurfaceBehind());
                }
            };
    private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
            new KeyguardUpdateMonitorCallback() {
                @Override
                public void onKeyguardDismissAnimationFinished() {
                    mShell.onKeyguardDismissAnimationFinished();
                }
            };
    private final UserTracker.Callback mUserChangedCallback =
            new UserTracker.Callback() {
                @Override
                public void onUserChanged(int newUser, @NonNull Context userContext) {
                    mShell.onUserChanged(newUser, userContext);
                }

                @Override
                public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
                    mShell.onUserProfilesChanged(profiles);
                }
            };

    private boolean mIsSysUiStateValid;
    private WakefulnessLifecycle.Observer mWakefulnessObserver;

    @Inject
    public WMShell(
            Context context,
            ShellInterface shell,
            Optional<Pip> pipOptional,
            Optional<SplitScreen> splitScreenOptional,
            Optional<OneHanded> oneHandedOptional,
            Optional<DesktopMode> desktopMode,
            Optional<RecentTasks> recentTasks,
            CommandQueue commandQueue,
            ConfigurationController configurationController,
            KeyguardStateController keyguardStateController,
            KeyguardUpdateMonitor keyguardUpdateMonitor,
            ScreenLifecycle screenLifecycle,
            SysUiState sysUiState,
            WakefulnessLifecycle wakefulnessLifecycle,
            UserTracker userTracker,
            DisplayTracker displayTracker,
            NoteTaskInitializer noteTaskInitializer,
            @Main Executor sysUiMainExecutor) {
        mContext = context;
        mShell = shell;
        mCommandQueue = commandQueue;
        mConfigurationController = configurationController;
        mKeyguardStateController = keyguardStateController;
        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
        mScreenLifecycle = screenLifecycle;
        mSysUiState = sysUiState;
        mPipOptional = pipOptional;
        mSplitScreenOptional = splitScreenOptional;
        mOneHandedOptional = oneHandedOptional;
        mDesktopModeOptional = desktopMode;
        mRecentTasksOptional = recentTasks;
        mWakefulnessLifecycle = wakefulnessLifecycle;
        mUserTracker = userTracker;
        mDisplayTracker = displayTracker;
        mNoteTaskInitializer = noteTaskInitializer;
        mSysUiMainExecutor = sysUiMainExecutor;
    }

    @Override
    public void start() {
        // Notify with the initial configuration and subscribe for new config changes
        mShell.onConfigurationChanged(mContext.getResources().getConfiguration());
        mConfigurationController.addCallback(mConfigurationListener);

        // Subscribe to keyguard changes
        mKeyguardStateController.addCallback(mKeyguardStateCallback);
        mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);

        // Subscribe to user changes
        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());

        mCommandQueue.addCallback(this);
        mPipOptional.ifPresent(this::initPip);
        mSplitScreenOptional.ifPresent(this::initSplitScreen);
        mOneHandedOptional.ifPresent(this::initOneHanded);
        mDesktopModeOptional.ifPresent(this::initDesktopMode);
        mRecentTasksOptional.ifPresent(this::initRecentTasks);

        mNoteTaskInitializer.initialize();
    }

    @VisibleForTesting
    void initPip(Pip pip) {
        mCommandQueue.addCallback(new CommandQueue.Callbacks() {
            @Override
            public void showPictureInPictureMenu() {
                pip.showPictureInPictureMenu();
            }
        });

        mSysUiState.addCallback(sysUiStateFlag -> {
            mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0;
            pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag);
        });
    }

    @VisibleForTesting
    void initSplitScreen(SplitScreen splitScreen) {
        mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
            @Override
            public void onFinishedWakingUp() {
                splitScreen.onFinishedWakingUp();
            }
        });
        mCommandQueue.addCallback(new CommandQueue.Callbacks() {
            @Override
            public void goToFullscreenFromSplit() {
                splitScreen.goToFullscreenFromSplit();
            }
        });
    }

    @VisibleForTesting
    void initOneHanded(OneHanded oneHanded) {
        oneHanded.registerTransitionCallback(new OneHandedTransitionCallback() {
            @Override
            public void onStartTransition(boolean isEntering) {
                mSysUiMainExecutor.execute(() -> {
                    mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
                            true).commitUpdate(mDisplayTracker.getDefaultDisplayId());
                });
            }

            @Override
            public void onStartFinished(Rect bounds) {
                mSysUiMainExecutor.execute(() -> {
                    mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
                            true).commitUpdate(mDisplayTracker.getDefaultDisplayId());
                });
            }

            @Override
            public void onStopFinished(Rect bounds) {
                mSysUiMainExecutor.execute(() -> {
                    mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
                            false).commitUpdate(mDisplayTracker.getDefaultDisplayId());
                });
            }
        });

        oneHanded.registerEventCallback(new OneHandedEventCallback() {
            @Override
            public void notifyExpandNotification() {
                mSysUiMainExecutor.execute(
                        () -> mCommandQueue.handleSystemKey(new KeyEvent(KeyEvent.ACTION_DOWN,
                                KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)));
            }
        });

        mWakefulnessObserver =
                new WakefulnessLifecycle.Observer() {
                    @Override
                    public void onFinishedWakingUp() {
                        // Reset locked for the case keyguard not shown.
                        oneHanded.setLockedDisabled(false /* locked */, false /* enabled */);
                    }

                    @Override
                    public void onStartedGoingToSleep() {
                        oneHanded.stopOneHanded();
                        // When user press power button going to sleep, temperory lock OHM disabled
                        // to avoid mis-trigger.
                        oneHanded.setLockedDisabled(true /* locked */, false /* enabled */);
                    }
                };
        mWakefulnessLifecycle.addObserver(mWakefulnessObserver);

        mScreenLifecycle.addObserver(new ScreenLifecycle.Observer() {
            @Override
            public void onScreenTurningOff() {
                oneHanded.stopOneHanded(
                        OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT);
            }
        });

        mCommandQueue.addCallback(new CommandQueue.Callbacks() {
            @Override
            public void onCameraLaunchGestureDetected(int source) {
                oneHanded.stopOneHanded();
            }

            @Override
            public void setImeWindowStatus(int displayId, IBinder token, int vis,
                    int backDisposition, boolean showImeSwitcher) {
                if (displayId == mDisplayTracker.getDefaultDisplayId()
                        && (vis & InputMethodService.IME_VISIBLE) != 0) {
                    oneHanded.stopOneHanded(
                            OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT);
                }
            }
        });
    }

    void initDesktopMode(DesktopMode desktopMode) {
        desktopMode.addVisibleTasksListener(
                new DesktopModeTaskRepository.VisibleTasksListener() {
                    @Override
                    public void onVisibilityChanged(int displayId, boolean hasFreeformTasks) {
                        if (displayId == Display.DEFAULT_DISPLAY) {
                            mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
                                            hasFreeformTasks)
                                    .commitUpdate(mDisplayTracker.getDefaultDisplayId());
                        }
                        // TODO(b/278084491): update sysui state for changes on other displays
                    }
                }, mSysUiMainExecutor);
    }

    @VisibleForTesting
    void initRecentTasks(RecentTasks recentTasks) {
        recentTasks.addAnimationStateListener(mSysUiMainExecutor,
                mCommandQueue::onRecentsAnimationStateChanged);
    }

    @Override
    public boolean isDumpCritical() {
        // Dump can't be critical because the shell has to dump on the main thread for
        // synchronization reasons, which isn't reliably fast.
        return false;
    }

    @Override
    public void dump(PrintWriter pw, String[] args) {
        Log.d(TAG, "Dumping with args: " + String.join(", ", args));

        // Strip out the SysUI "dependency" arg before sending to WMShell
        if (args[0].equals("dependency")) {
            args = Arrays.copyOfRange(args, 1, args.length);
        }

        // Handle commands if provided
        if (mShell.handleCommand(args, pw)) {
            return;
        }
        // Dump WMShell stuff here if no commands were handled
        mShell.dump(pw);
    }
}
