// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.ui.fold_transitions;

import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;

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

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider.LayoutStateObserver;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.omnibox.OmniboxFocusReason;
import org.chromium.chrome.browser.toolbar.ToolbarManager;
import org.chromium.chrome.features.start_surface.StartSurface;
import org.chromium.ui.KeyboardVisibilityDelegate;

/**
 * A utility class to handle saving and restoring the UI state across fold transitions.
 */
public class FoldTransitionController {
    public static final String DID_CHANGE_TABLET_MODE = "did_change_tablet_mode";
    public static final String RESUME_HOME_SURFACE_ON_MODE_CHANGE =
            "resume_home_surface_on_mode_change";
    public static final long KEYBOARD_RESTORATION_TIMEOUT_MS = 2 * 1000; // 2 seconds
    static final String URL_BAR_FOCUS_STATE = "url_bar_focus_state";
    static final String URL_BAR_EDIT_TEXT = "url_bar_edit_text";
    static final String KEYBOARD_VISIBILITY_STATE = "keyboard_visibility_state";
    static final String TAB_SWITCHER_VISIBILITY_STATE = "tab_switcher_visibility_state";

    private final OneshotSupplier<ToolbarManager> mToolbarManagerSupplier;
    private final ObservableSupplier<LayoutManager> mLayoutManagerSupplier;
    private final ActivityTabProvider mActivityTabProvider;
    private final Handler mLayoutStateHandler;
    private final OneshotSupplier<StartSurface> mStartSurfaceSupplier;
    private boolean mKeyboardVisibleDuringFoldTransition;
    private Long mKeyboardVisibilityTimestamp;

    /**
     * Construct a {@link FoldTransitionController} instance.
     *
     * @param toolbarManagerSupplier The {@link ToolbarManager} instance supplier.
     * @param layoutManagerSupplier The {@link LayoutManager} instance supplier.
     * @param activityTabProvider The current activity tab provider.
     * @param layoutStateHandler The {@link Handler} to post UI state restoration.
     */
    public FoldTransitionController(
            @NonNull OneshotSupplierImpl<ToolbarManager> toolbarManagerSupplier,
            @NonNull ObservableSupplier<LayoutManager> layoutManagerSupplier,
            @NonNull ActivityTabProvider activityTabProvider,
            @NonNull OneshotSupplier<StartSurface> startSurfaceSupplier,
            Handler layoutStateHandler) {
        mToolbarManagerSupplier = toolbarManagerSupplier;
        mLayoutManagerSupplier = layoutManagerSupplier;
        mActivityTabProvider = activityTabProvider;
        mLayoutStateHandler = layoutStateHandler;
        mStartSurfaceSupplier = startSurfaceSupplier;
    }

    /**
     * Saves the relevant UI when the activity is recreated on a device fold transition. Expected to
     * be invoked during {@code Activity#onSaveInstanceState()}.
     *
     * @param savedInstanceState The {@link Bundle} where the UI state will be saved.
     * @param didChangeTabletMode Whether the activity is recreated due to a fold configuration
     *         change. {@code true} if the fold configuration changed, {@code false} otherwise.
     * @param isIncognito Whether the current TabModel is incognito mode.
     */
    public void saveUiState(
            Bundle savedInstanceState, boolean didChangeTabletMode, boolean isIncognito) {
        if (savedInstanceState == null) return;

        savedInstanceState.putBoolean(DID_CHANGE_TABLET_MODE, didChangeTabletMode);
        if (mToolbarManagerSupplier.hasValue() && mToolbarManagerSupplier.get().isUrlBarFocused()) {
            savedInstanceState.putBoolean(URL_BAR_FOCUS_STATE, true);
            savedInstanceState.putString(URL_BAR_EDIT_TEXT,
                    mToolbarManagerSupplier.get().getUrlBarTextWithoutAutocomplete());
        }

        if (getKeyboardVisibilityState()) {
            savedInstanceState.putBoolean(KEYBOARD_VISIBILITY_STATE, true);
        }

        if (mLayoutManagerSupplier.hasValue()) {
            // Start surface used a TAB_SWITCHER LayoutType before the START_SURFACE_REFACTOR
            // feature was supported. Therefore, it is safe to assume that the Tab switcher layout
            // is actually visible only when the Start surface home page is not showing, and the Tab
            // switcher visibility state will be saved to the instance state only in this case.
            // TODO(https://crbug.com/1315676): Clean up the logic once the START_SURFACE_REFACTOR
            // flag is enabled by default.
            if (mLayoutManagerSupplier.get().isLayoutVisible(LayoutType.TAB_SWITCHER)
                    || mLayoutManagerSupplier.get().isLayoutVisible(LayoutType.START_SURFACE)) {
                if (mStartSurfaceSupplier.hasValue()
                        && mStartSurfaceSupplier.get().isHomepageShown()) {
                    saveHomeSurfaceState(
                            savedInstanceState, mStartSurfaceSupplier.get(), isIncognito);
                } else {
                    savedInstanceState.putBoolean(TAB_SWITCHER_VISIBILITY_STATE, true);
                }
            }
        }
    }

    /**
     * Restores the relevant UI state when the activity is recreated on a device fold transition.
     *
     * @param savedInstanceState The {@link Bundle} that is used to restore the UI state.
     */
    public void restoreUiState(Bundle savedInstanceState) {
        if (savedInstanceState == null || !mLayoutManagerSupplier.hasValue()) {
            return;
        }

        // Restore the UI state only on a device fold transition.
        if (!savedInstanceState.getBoolean(DID_CHANGE_TABLET_MODE, false)) {
            return;
        }

        restoreOmniboxState(savedInstanceState, mToolbarManagerSupplier.get(),
                mLayoutManagerSupplier.get(), mLayoutStateHandler);
        restoreKeyboardState(savedInstanceState, mActivityTabProvider, mLayoutManagerSupplier.get(),
                mLayoutStateHandler);
        restoreTabSwitcherState(savedInstanceState, mLayoutManagerSupplier.get());
    }

    /**
     * Saves the Home surface state on a phone to tablet configuration change.
     */
    @VisibleForTesting
    static void saveHomeSurfaceState(
            Bundle savedInstanceState, StartSurface startSurface, boolean isIncognito) {
        if (savedInstanceState == null || startSurface == null || isIncognito
                || !savedInstanceState.getBoolean(DID_CHANGE_TABLET_MODE)) {
            return;
        }

        savedInstanceState.putBoolean(RESUME_HOME_SURFACE_ON_MODE_CHANGE, true);
    }

    boolean getKeyboardVisibleDuringFoldTransitionForTesting() {
        return mKeyboardVisibleDuringFoldTransition;
    }

    Long getKeyboardVisibilityTimestampForTesting() {
        return mKeyboardVisibilityTimestamp;
    }

    private boolean getKeyboardVisibilityState() {
        if (!shouldSaveKeyboardState(mActivityTabProvider)) {
            return false;
        }

        var actualKeyboardVisibilityState = false;
        var keyboardVisible = isKeyboardVisible(mActivityTabProvider);
        if (keyboardVisible) {
            // The keyboard is currently visible.
            actualKeyboardVisibilityState = true;
            mKeyboardVisibleDuringFoldTransition = true;
            mKeyboardVisibilityTimestamp = SystemClock.elapsedRealtime();
        } else if (mKeyboardVisibleDuringFoldTransition) {
            // This is to handle the case when folding a device invokes Activity#onStop twice
            // (see crbug.com/1426678 for details), thereby invoking #onSaveInstanceState twice.
            // In this flow, Activity#onPause is also invoked twice, and the first call to
            // #onPause hides the keyboard if it is visible, while also clearing the previous
            // instance state. The actual keyboard visibility state during the second invocation
            // is determined by |mKeyboardVisibleDuringFoldTransition| that will be used only if
            // it is valid in terms of a timeout within which the fold transition occurs, to
            // avoid erroneously setting the keyboard state under other circumstances if
            // |mKeyboardVisibleDuringFoldTransition| is not reset.
            if (isKeyboardStateValid(mKeyboardVisibilityTimestamp)) {
                actualKeyboardVisibilityState = true;
            }
            mKeyboardVisibleDuringFoldTransition = false;
            mKeyboardVisibilityTimestamp = null;
        }
        return actualKeyboardVisibilityState;
    }

    /**
     * Determines whether the keyboard state should be saved during a fold transition. The keyboard
     * state will be saved only if the web contents has a focused editable node.
     *
     * @param activityTabProvider The current activity tab provider.
     * @return {@code true} if the keyboard state should be saved, {@code false} otherwise.
     */
    private static boolean shouldSaveKeyboardState(ActivityTabProvider activityTabProvider) {
        if (activityTabProvider.get() == null
                || activityTabProvider.get().getWebContents() == null) {
            return false;
        }
        return activityTabProvider.get().getWebContents().isFocusedElementEditable();
    }

    /**
     * Determines whether the soft keyboard is visible.
     *
     * @param activityTabProvider The current activity tab provider.
     * @return {@code true} if the keyboard is visible, {@code false} otherwise.
     */
    private static boolean isKeyboardVisible(@NonNull ActivityTabProvider activityTabProvider) {
        if (activityTabProvider.get() == null || activityTabProvider.get().getWebContents() == null
                || activityTabProvider.get().getWebContents().getViewAndroidDelegate() == null) {
            return false;
        }

        return KeyboardVisibilityDelegate.getInstance().isKeyboardShowing(
                activityTabProvider.get().getContext(),
                activityTabProvider.get()
                        .getWebContents()
                        .getViewAndroidDelegate()
                        .getContainerView());
    }

    /**
     * Determines whether the keyboard visibility state is valid for restoration after a fold
     * transition.
     *
     * @param timestamp The time (in milliseconds) at which the keyboard visibility state was saved
     *         during a fold transition.
     * @return {@code true} if the keyboard visibility state is valid for restoration, {@code false}
     *         otherwise.
     */
    private static boolean isKeyboardStateValid(Long timestamp) {
        return timestamp != null
                && SystemClock.elapsedRealtime() - timestamp <= KEYBOARD_RESTORATION_TIMEOUT_MS;
    }

    private static void restoreUiStateOnLayoutDoneShowing(LayoutManager layoutManager,
            Handler layoutStateHandler, Runnable onLayoutFinishedShowing) {
        /* TODO (crbug/1395495): Restore the UI state directly if the invocation of {@code
         * StaticLayout#requestFocus(Tab)} in {@code StaticLayout#doneShowing()} is removed. We
         * should restore the desired UI state after the {@link StaticLayout} is done showing to
         * persist the state. If the layout is visible and done showing, it is safe to execute the
         * UI restoration runnable directly to persist the desired UI state. */
        if (layoutManager.isLayoutVisible(LayoutType.BROWSING)
                && !layoutManager.isLayoutStartingToShow(LayoutType.BROWSING)) {
            onLayoutFinishedShowing.run();
        } else {
            layoutManager.addObserver(new LayoutStateObserver() {
                @Override
                public void onFinishedShowing(int layoutType) {
                    assert layoutManager.isLayoutVisible(LayoutType.BROWSING)
                        : "LayoutType is "
                            + layoutManager.getActiveLayoutType()
                            + ", expected BROWSING type on activity start.";
                    LayoutStateObserver.super.onFinishedShowing(layoutType);
                    layoutStateHandler.post(() -> {
                        onLayoutFinishedShowing.run();
                        layoutManager.removeObserver(this);
                        layoutStateHandler.removeCallbacksAndMessages(null);
                    });
                }
            });
        }
    }

    private static void restoreOmniboxState(@NonNull Bundle savedInstanceState,
            ToolbarManager toolbarManager, @NonNull LayoutManager layoutManager,
            Handler layoutStateHandler) {
        if (toolbarManager == null || !savedInstanceState.getBoolean(URL_BAR_FOCUS_STATE, false)) {
            return;
        }
        String urlBarText = savedInstanceState.getString(URL_BAR_EDIT_TEXT, "");
        restoreUiStateOnLayoutDoneShowing(layoutManager, layoutStateHandler,
                () -> setUrlBarFocusAndText(toolbarManager, urlBarText));
    }

    private static void restoreKeyboardState(@NonNull Bundle savedInstanceState,
            @NonNull ActivityTabProvider activityTabProvider, @NonNull LayoutManager layoutManager,
            Handler layoutStateHandler) {
        // Restore the keyboard only if the omnibox focus was not restored, because omnibox code
        // is assumed to restore the keyboard on omnibox focus restoration.
        if (savedInstanceState.getBoolean(URL_BAR_FOCUS_STATE, false)) {
            return;
        }
        if (!savedInstanceState.getBoolean(KEYBOARD_VISIBILITY_STATE, false)) {
            return;
        }
        restoreUiStateOnLayoutDoneShowing(
                layoutManager, layoutStateHandler, () -> showSoftInput(activityTabProvider));
    }

    private static void restoreTabSwitcherState(
            @NonNull Bundle savedInstanceState, @NonNull LayoutManager layoutManager) {
        if (!savedInstanceState.getBoolean(TAB_SWITCHER_VISIBILITY_STATE, false)) {
            return;
        }
        layoutManager.showLayout(LayoutType.TAB_SWITCHER, false);
    }

    private static void setUrlBarFocusAndText(ToolbarManager toolbarManager, String urlBarText) {
        toolbarManager.setUrlBarFocusAndText(
                true, OmniboxFocusReason.FOLD_TRANSITION_RESTORATION, urlBarText);
    }

    private static void showSoftInput(@NonNull ActivityTabProvider activityTabProvider) {
        var tab = activityTabProvider.get();
        if (tab == null) {
            return;
        }
        var webContents = tab.getWebContents();
        if (webContents == null || webContents.getViewAndroidDelegate() == null) {
            return;
        }

        var containerView = webContents.getViewAndroidDelegate().getContainerView();
        webContents.scrollFocusedEditableNodeIntoView();
        KeyboardVisibilityDelegate.getInstance().showKeyboard(containerView);
    }
}
