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

import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
import static com.android.systemui.animation.Interpolators.EMPHASIZED;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.util.Log;

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

import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.util.window.RefreshRateTracker;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.animation.ViewRootSync;
import com.android.systemui.shared.recents.model.ThumbnailData;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.StringJoiner;

/**
 * Track LauncherState, RecentsAnimation, resumed state for task bar in one place here and animate
 * the task bar accordingly.
 */
 public class TaskbarLauncherStateController {

    private static final String TAG = TaskbarLauncherStateController.class.getSimpleName();
    private static final boolean DEBUG = false;

    public static final int FLAG_RESUMED = 1 << 0;
    public static final int FLAG_RECENTS_ANIMATION_RUNNING = 1 << 1;
    public static final int FLAG_TRANSITION_STATE_RUNNING = 1 << 2;

    private static final int FLAGS_LAUNCHER = FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING;
    /** Equivalent to an int with all 1s for binary operation purposes */
    private static final int FLAGS_ALL = ~0;

    private final AnimatedFloat mIconAlignment =
            new AnimatedFloat(this::onIconAlignmentRatioChanged);

    private TaskbarControllers mControllers;
    private AnimatedFloat mTaskbarBackgroundAlpha;
    private AnimatedFloat mTaskbarCornerRoundness;
    private MultiProperty mIconAlphaForHome;
    private QuickstepLauncher mLauncher;

    private Integer mPrevState;
    private int mState;
    private LauncherState mLauncherState = LauncherState.NORMAL;

    private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener;

    private boolean mIsAnimatingToLauncher;

    private boolean mShouldDelayLauncherStateAnim;

    // We skip any view synchronizations during init/destroy.
    private boolean mCanSyncViews;

    private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
            dp -> updateIconAlphaForHome(mIconAlphaForHome.getValue());

    private final StateManager.StateListener<LauncherState> mStateListener =
            new StateManager.StateListener<LauncherState>() {

                @Override
                public void onStateTransitionStart(LauncherState toState) {
                    if (toState != mLauncherState) {
                        // Treat FLAG_TRANSITION_STATE_RUNNING as a changed flag even if a previous
                        // state transition was already running, so we update the new target.
                        mPrevState &= ~FLAG_TRANSITION_STATE_RUNNING;
                        mLauncherState = toState;
                    }
                    updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, true);
                    if (!mShouldDelayLauncherStateAnim) {
                        if (toState == LauncherState.NORMAL) {
                            applyState(QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION);
                        } else {
                            applyState();
                        }
                    }
                }

                @Override
                public void onStateTransitionComplete(LauncherState finalState) {
                    mLauncherState = finalState;
                    updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false);
                    applyState();
                    boolean disallowLongClick = finalState == LauncherState.OVERVIEW_SPLIT_SELECT;
                    com.android.launcher3.taskbar.Utilities.setOverviewDragState(
                            mControllers, finalState.disallowTaskbarGlobalDrag(),
                            disallowLongClick, finalState.allowTaskbarInitialSplitSelection());
                }
            };

    public void init(TaskbarControllers controllers, QuickstepLauncher launcher) {
        mCanSyncViews = false;

        mControllers = controllers;
        mLauncher = launcher;

        mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
                .getTaskbarBackgroundAlpha();
        mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
        mIconAlphaForHome = mControllers.taskbarViewController
                .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);

        resetIconAlignment();

        mLauncher.getStateManager().addStateListener(mStateListener);

        // Initialize to the current launcher state
        updateStateForFlag(FLAG_RESUMED, launcher.hasBeenResumed());
        mLauncherState = launcher.getStateManager().getState();
        applyState(0);

        mCanSyncViews = true;
        mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
    }

    public void onDestroy() {
        mCanSyncViews = false;

        mIconAlignment.finishAnimation();

        Log.d("b/260135164", "onDestroy - updateIconAlphaForHome(1)");
        mLauncher.getHotseat().setIconsAlpha(1f);
        mLauncher.getStateManager().removeStateListener(mStateListener);

        mCanSyncViews = true;
        mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
    }

    public Animator createAnimToLauncher(@NonNull LauncherState toState,
            @NonNull RecentsAnimationCallbacks callbacks, long duration) {
        // If going to overview, stash the task bar
        // If going home, align the icons to hotseat
        AnimatorSet animatorSet = new AnimatorSet();

        // Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly.
        TaskbarStashController stashController = mControllers.taskbarStashController;
        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE,
                toState.isTaskbarStashed(mLauncher));
        if (DEBUG) {
            Log.d(TAG, "createAnimToLauncher - FLAG_IN_APP: " + false);
        }
        stashController.updateStateForFlag(FLAG_IN_APP, false);

        updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, true);
        animatorSet.play(stashController.createApplyStateAnimator(duration));
        animatorSet.play(applyState(duration, false));

        if (mTaskBarRecentsAnimationListener != null) {
            mTaskBarRecentsAnimationListener.endGestureStateOverride(
                    !mLauncher.isInState(LauncherState.OVERVIEW));
        }
        mTaskBarRecentsAnimationListener = new TaskBarRecentsAnimationListener(callbacks);
        callbacks.addListener(mTaskBarRecentsAnimationListener);
        ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(() ->
                mTaskBarRecentsAnimationListener.endGestureStateOverride(true));
        return animatorSet;
    }

    public boolean isAnimatingToLauncher() {
        return mIsAnimatingToLauncher;
    }

    public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) {
        if (!shouldDelayLauncherStateAnim && mShouldDelayLauncherStateAnim) {
            // Animate the animation we have delayed immediately. This is usually triggered when
            // the user has released their finger.
            applyState();
        }
        mShouldDelayLauncherStateAnim = shouldDelayLauncherStateAnim;
    }

    /**
     * Updates the proper flag to change the state of the task bar.
     *
     * Note that this only updates the flag. {@link #applyState()} needs to be called separately.
     *
     * @param flag The flag to update.
     * @param enabled Whether to enable the flag
     */
    public void updateStateForFlag(int flag, boolean enabled) {
        if (enabled) {
            mState |= flag;
        } else {
            mState &= ~flag;
        }
    }

    private boolean hasAnyFlag(int flagMask) {
        return hasAnyFlag(mState, flagMask);
    }

    private boolean hasAnyFlag(int flags, int flagMask) {
        return (flags & flagMask) != 0;
    }

    public void applyState() {
        applyState(mControllers.taskbarStashController.getStashDuration());
    }

    public void applyState(long duration) {
        applyState(duration, true);
    }

    public Animator applyState(boolean start) {
        return applyState(mControllers.taskbarStashController.getStashDuration(), start);
    }

    public Animator applyState(long duration, boolean start) {
        if (mControllers.taskbarActivityContext.isDestroyed()) {
            return null;
        }
        Animator animator = null;
        if (mPrevState == null || mPrevState != mState) {
            // If this is our initial state, treat all flags as changed.
            int changedFlags = mPrevState == null ? FLAGS_ALL : mPrevState ^ mState;
            mPrevState = mState;
            animator = onStateChangeApplied(changedFlags, duration, start);
        }
        return animator;
    }

    private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
        final boolean goingToLauncher = isInLauncher();
        final float toAlignment = isIconAlignedWithHotseat() ? 1 : 0;
        boolean handleOpenFloatingViews = false;
        if (DEBUG) {
            Log.d(TAG, "onStateChangeApplied - mState: " + getStateString(mState)
                    + ", changedFlags: " + getStateString(changedFlags)
                    + ", goingToLauncher: " + goingToLauncher
                    + ", mLauncherState: " + mLauncherState
                    + ", toAlignment: " + toAlignment);
        }
        AnimatorSet animatorSet = new AnimatorSet();

        // Add the state animation first to ensure FLAG_IN_STASHED_LAUNCHER_STATE is set and we can
        // determine whether goingToUnstashedLauncherStateChanged.
        if (hasAnyFlag(changedFlags, FLAG_TRANSITION_STATE_RUNNING)) {
            boolean committed = !hasAnyFlag(FLAG_TRANSITION_STATE_RUNNING);
            playStateTransitionAnim(animatorSet, duration, committed);

            if (committed && mLauncherState == LauncherState.QUICK_SWITCH_FROM_HOME) {
                // We're about to be paused, set immediately to ensure seamless handoff.
                updateStateForFlag(FLAG_RESUMED, false);
                applyState(0 /* duration */);
            }
            if (mLauncherState == LauncherState.NORMAL) {
                // We're changing state to home, should close open popups e.g. Taskbar AllApps
                handleOpenFloatingViews = true;
            }
        }

        if (hasAnyFlag(changedFlags, FLAGS_LAUNCHER)) {
            animatorSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mIsAnimatingToLauncher = false;
                }

                @Override
                public void onAnimationStart(Animator animation) {
                    mIsAnimatingToLauncher = goingToLauncher;

                    TaskbarStashController stashController =
                            mControllers.taskbarStashController;
                    if (DEBUG) {
                        Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !goingToLauncher);
                    }
                    stashController.updateStateForFlag(FLAG_IN_APP, !goingToLauncher);
                    stashController.applyState(duration);
                }
            });

            // Handle closing open popups when going home/overview
            handleOpenFloatingViews = true;
        }
        if (handleOpenFloatingViews && goingToLauncher) {
            AbstractFloatingView.closeAllOpenViews(mControllers.taskbarActivityContext);
        }

        float backgroundAlpha =
                goingToLauncher && mLauncherState.isTaskbarAlignedWithHotseat(mLauncher)
                        ? 0 : 1;
        // Don't animate if background has reached desired value.
        if (mTaskbarBackgroundAlpha.isAnimating()
                || mTaskbarBackgroundAlpha.value != backgroundAlpha) {
            mTaskbarBackgroundAlpha.cancelAnimation();
            if (DEBUG) {
                Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - "
                        + mTaskbarBackgroundAlpha.value
                        + " -> " + backgroundAlpha + ": " + duration);
            }

            Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
                    .animateToValue(backgroundAlpha)
                    .setDuration(duration);
            // Add a single frame delay to the taskbar bg to avoid too many moving parts during the
            // app launch animation.
            taskbarBackgroundAlpha.setStartDelay(
                    (hasAnyFlag(changedFlags, FLAG_RESUMED) && !goingToLauncher)
                            ? RefreshRateTracker.getSingleFrameMs(mLauncher)
                            : 0);
            animatorSet.play(taskbarBackgroundAlpha);
        }

        float cornerRoundness = goingToLauncher ? 0 : 1;
        // Don't animate if corner roundness has reached desired value.
        if (mTaskbarCornerRoundness.isAnimating()
                || mTaskbarCornerRoundness.value != cornerRoundness) {
            mTaskbarCornerRoundness.cancelAnimation();
            if (DEBUG) {
                Log.d(TAG, "onStateChangeApplied - taskbarCornerRoundness - "
                        + mTaskbarCornerRoundness.value
                        + " -> " + cornerRoundness + ": " + duration);
            }
            animatorSet.play(mTaskbarCornerRoundness.animateToValue(cornerRoundness));
        }

        if (mIconAlignment.isAnimatingToValue(toAlignment)
                || mIconAlignment.isSettledOnValue(toAlignment)) {
            // Already at desired value, but make sure we run the callback at the end.
            animatorSet.addListener(AnimatorListeners.forEndCallback(
                    this::onIconAlignmentRatioChanged));
        } else {
            mIconAlignment.cancelAnimation();
            ObjectAnimator iconAlignAnim = mIconAlignment
                    .animateToValue(toAlignment)
                    .setDuration(duration);
            if (DEBUG) {
                Log.d(TAG, "onStateChangeApplied - iconAlignment - "
                        + mIconAlignment.value
                        + " -> " + toAlignment + ": " + duration);
            }
            animatorSet.play(iconAlignAnim);
        }

        animatorSet.setInterpolator(EMPHASIZED);

        if (start) {
            animatorSet.start();
        }
        return animatorSet;
    }

    /** Returns whether we're going to a state where taskbar icons should align with launcher. */
    public boolean goingToAlignedLauncherState() {
        return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
    }

    /**
     * Returns if icons should be aligned to hotseat in the current transition
     */
    public boolean isIconAlignedWithHotseat() {
        if (isInLauncher()) {
            boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
            boolean willStashVisually = isInStashedState
                    && mControllers.taskbarStashController.supportsVisualStashing();
            boolean isTaskbarAlignedWithHotseat =
                    mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
            return isTaskbarAlignedWithHotseat && !willStashVisually;
        } else {
            return false;
        }
    }

    /**
     * Returns if the current Launcher state has hotseat on top of other elemnets.
     */
    public boolean isInHotseatOnTopStates() {
        return mLauncherState != LauncherState.ALL_APPS;
    }

    private void playStateTransitionAnim(AnimatorSet animatorSet, long duration,
            boolean committed) {
        boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
        TaskbarStashController stashController = mControllers.taskbarStashController;
        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, isInStashedState);
        Animator stashAnimator = stashController.createApplyStateAnimator(duration);
        if (stashAnimator != null) {
            stashAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (isInStashedState && committed) {
                        // Reset hotseat alpha to default
                        Log.d("b/260135164",
                                "playStateTransitionAnim#onAnimationEnd - setIconsAlpha(1)");
                        mLauncher.getHotseat().setIconsAlpha(1);
                    }
                }

                @Override
                public void onAnimationStart(Animator animation) {
                    if (mLauncher.getHotseat().getIconsAlpha() > 0) {
                        updateIconAlphaForHome(mLauncher.getHotseat().getIconsAlpha());
                    }
                }
            });
            animatorSet.play(stashAnimator);
        }
    }

    private boolean isInLauncher() {
        return (mState & FLAGS_LAUNCHER) != 0;
    }

    /**
     * Resets and updates the icon alignment.
     */
    protected void resetIconAlignment() {
        mIconAlignment.finishAnimation();
        onIconAlignmentRatioChanged();
    }

    private void onIconAlignmentRatioChanged() {
        float currentValue = mIconAlphaForHome.getValue();
        boolean taskbarWillBeVisible = mIconAlignment.value < 1;
        boolean firstFrameVisChanged = (taskbarWillBeVisible && Float.compare(currentValue, 1) != 0)
                || (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0);

        mControllers.taskbarViewController.setLauncherIconAlignment(
                mIconAlignment.value, mLauncher.getDeviceProfile());
        mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value);
        // Switch taskbar and hotseat in last frame
        updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0);

        // Sync the first frame where we swap taskbar and hotseat.
        if (firstFrameVisChanged && mCanSyncViews && !Utilities.IS_RUNNING_IN_TEST_HARNESS) {
            ViewRootSync.synchronizeNextDraw(mLauncher.getHotseat(),
                    mControllers.taskbarActivityContext.getDragLayer(),
                    () -> {});
        }
    }

    private void updateIconAlphaForHome(float alpha) {
        if (mControllers.taskbarActivityContext.isDestroyed()) {
            Log.e("b/260135164", "updateIconAlphaForHome is called after Taskbar is destroyed",
                    new Exception());
        }
        mIconAlphaForHome.setValue(alpha);
        boolean hotseatVisible = alpha == 0
                || (!mControllers.uiController.isHotseatIconOnTopWhenAligned()
                && mIconAlignment.value > 0);
        /*
         * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets
         * should not be visible at the same time.
         */
        Log.d("b/260135164",
                "updateIconAlphaForHome - setIconsAlpha(" + (hotseatVisible ? 1 : 0)
                        + "), isTaskbarPresent: " + mLauncher.getDeviceProfile().isTaskbarPresent);
        mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0);
        mLauncher.getHotseat().setQsbAlpha(
                mLauncher.getDeviceProfile().isQsbInline && !hotseatVisible ? 0 : 1);
    }

    private final class TaskBarRecentsAnimationListener implements
            RecentsAnimationCallbacks.RecentsAnimationListener {
        private final RecentsAnimationCallbacks mCallbacks;

        TaskBarRecentsAnimationListener(RecentsAnimationCallbacks callbacks) {
            mCallbacks = callbacks;
        }

        @Override
        public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
            boolean isInOverview = mLauncher.isInState(LauncherState.OVERVIEW);
            endGestureStateOverride(!isInOverview);
        }

        @Override
        public void onRecentsAnimationFinished(RecentsAnimationController controller) {
            endGestureStateOverride(!controller.getFinishTargetIsLauncher());
        }

        private void endGestureStateOverride(boolean finishedToApp) {
            mCallbacks.removeListener(this);
            mTaskBarRecentsAnimationListener = null;
            ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);

            // Update the resumed state immediately to ensure a seamless handoff
            boolean launcherResumed = !finishedToApp;
            updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, false);
            updateStateForFlag(FLAG_RESUMED, launcherResumed);
            applyState();

            TaskbarStashController controller = mControllers.taskbarStashController;
            if (DEBUG) {
                Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
            }
            controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
            controller.applyState();
        }
    }

    private static String getStateString(int flags) {
        StringJoiner str = new StringJoiner("|");
        if ((flags & FLAG_RESUMED) != 0) {
            str.add("FLAG_RESUMED");
        }
        if ((flags & FLAG_RECENTS_ANIMATION_RUNNING) != 0) {
            str.add("FLAG_RECENTS_ANIMATION_RUNNING");
        }
        if ((flags & FLAG_TRANSITION_STATE_RUNNING) != 0) {
            str.add("FLAG_TRANSITION_STATE_RUNNING");
        }
        return str.toString();
    }

    protected void dumpLogs(String prefix, PrintWriter pw) {
        pw.println(prefix + "TaskbarLauncherStateController:");
        pw.println(String.format(
                "%s\tmIconAlignment=%.2f",
                prefix,
                mIconAlignment.value));
        pw.println(String.format(
                "%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value));
        pw.println(String.format(
                "%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue()));
        pw.println(String.format("%s\tmPrevState=%s", prefix, getStateString(mPrevState)));
        pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState)));
        pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState));
        pw.println(String.format(
                "%s\tmIsAnimatingToLauncher=%b",
                prefix,
                mIsAnimatingToLauncher));
        pw.println(String.format(
                "%s\tmShouldDelayLauncherStateAnim=%b", prefix, mShouldDelayLauncherStateAnim));
    }
}
