package com.android.uiuios.allapps;

import static com.android.uiuios.LauncherState.ALL_APPS_CONTENT;
import static com.android.uiuios.LauncherState.ALL_APPS_HEADER_EXTRA;
import static com.android.uiuios.LauncherState.BACKGROUND_APP;
import static com.android.uiuios.LauncherState.HOTSEAT_ICONS;
import static com.android.uiuios.LauncherState.OVERVIEW;
import static com.android.uiuios.LauncherState.VERTICAL_SWIPE_INDICATOR;
import static com.android.uiuios.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
import static com.android.uiuios.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
import static com.android.uiuios.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
import static com.android.uiuios.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.uiuios.anim.Interpolators.LINEAR;
import static com.android.uiuios.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
import static com.android.uiuios.util.SystemUiController.UI_STATE_ALL_APPS;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.util.FloatProperty;
import android.util.Log;
import android.view.animation.Interpolator;

import com.android.uiuios.DeviceProfile;
import com.android.uiuios.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.uiuios.Launcher;
import com.android.uiuios.LauncherState;
import com.android.uiuios.LauncherStateManager.AnimationConfig;
import com.android.uiuios.LauncherStateManager.StateHandler;
import com.android.uiuios.R;
import com.android.uiuios.anim.AnimationSuccessListener;
import com.android.uiuios.anim.AnimatorSetBuilder;
import com.android.uiuios.anim.PropertySetter;
import com.android.uiuios.anim.SpringObjectAnimator;
import com.android.uiuios.testing.TestProtocol;
import com.android.uiuios.util.Themes;
import com.android.uiuios.views.ScrimView;

/**
 * Handles AllApps view transition.
 * 1) Slides all apps view using direct manipulation
 * 2) When finger is released, animate to either top or bottom accordingly.
 * <p/>
 * Algorithm:
 * If release velocity > THRES1, snap according to the direction of movement.
 * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
 * closer to top or closer to the page indicator.
 */
public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener {

    private static final float SPRING_DAMPING_RATIO = 0.9f;
    private static final float SPRING_STIFFNESS = 600f;

    public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
            new FloatProperty<AllAppsTransitionController>("allAppsProgress") {

        @Override
        public Float get(AllAppsTransitionController controller) {
            return controller.mProgress;
        }

        @Override
        public void setValue(AllAppsTransitionController controller, float progress) {
            controller.setProgress(progress);
        }
    };

    private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0;

    private AllAppsContainerView mAppsView;
    private ScrimView mScrimView;

    private final Launcher mLauncher;
    private final boolean mIsDarkTheme;
    private boolean mIsVerticalLayout;

    // Animation in this class is controlled by a single variable {@link mProgress}.
    // Visually, it represents top y coordinate of the all apps container if multiplied with
    // {@link mShiftRange}.

    // When {@link mProgress} is 0, all apps container is pulled up.
    // When {@link mProgress} is 1, all apps container is pulled down.
    private float mShiftRange;      // changes depending on the orientation
    private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent

    private float mScrollRangeDelta = 0;

    public AllAppsTransitionController(Launcher l) {
        mLauncher = l;
        mShiftRange = mLauncher.getDeviceProfile().heightPx;
        mProgress = 1f;

        mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
        mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout();
        mLauncher.addOnDeviceProfileChangeListener(this);
    }

    public float getShiftRange() {
        return mShiftRange;
    }

    @Override
    public void onDeviceProfileChanged(DeviceProfile dp) {
        mIsVerticalLayout = dp.isVerticalBarLayout();
        setScrollRangeDelta(mScrollRangeDelta);

        if (mIsVerticalLayout) {
            mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1);
            mLauncher.getHotseat().setTranslationY(0);
            mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
        }
    }

    /**
     * Note this method should not be called outside this class. This is public because it is used
     * in xml-based animations which also handle updating the appropriate UI.
     *
     * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
     *
     * @see #setState(LauncherState)
     * @see #setStateWithAnimation(LauncherState, AnimatorSetBuilder, AnimationConfig)
     */
    public void setProgress(float progress) {
        mProgress = progress;
        mScrimView.setProgress(progress);
        float shiftCurrent = progress * mShiftRange;

        mAppsView.setTranslationY(shiftCurrent);

        // Use a light system UI (dark icons) if all apps is behind at least half of the
        // status bar.
        boolean forceChange = shiftCurrent - mScrimView.getDragHandleSize()
                <= mLauncher.getDeviceProfile().getInsets().top / 2;
        if (forceChange) {
            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, !mIsDarkTheme);
        } else {
            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0);
        }

        if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
            // Translate hotseat with the shelf until reaching overview.
            float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
            if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
                float hotseatShift = (progress - overviewProgress) * mShiftRange;
                mLauncher.getHotseat().setTranslationY(hotseatShift);
            }
        }
    }

    public float getProgress() {
        return mProgress;
    }

    /**
     * Sets the vertical transition progress to {@param state} and updates all the dependent UI
     * accordingly.
     */
    @Override
    public void setState(LauncherState state) {
        setProgress(state.getVerticalProgress(mLauncher));
        setAlphas(state, null, new AnimatorSetBuilder());
        onProgressAnimationEnd();
    }

    /**
     * Creates an animation which updates the vertical transition progress and updates all the
     * dependent UI using various animation events
     */
    @Override
    public void setStateWithAnimation(LauncherState toState,
            AnimatorSetBuilder builder, AnimationConfig config) {
        if (TestProtocol.sDebugTracing) {
            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG,
                    "setStateWithAnimation " + toState.getClass().getSimpleName());
        }
        float targetProgress = toState.getVerticalProgress(mLauncher);
        if (Float.compare(mProgress, targetProgress) == 0) {
            setAlphas(toState, config, builder);
            // Fail fast
            onProgressAnimationEnd();
            return;
        }

        if (!config.playNonAtomicComponent()) {
            // There is no atomic component for the all apps transition, so just return early.
            return;
        }

        Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW
                ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
                : FAST_OUT_SLOW_IN;
        Animator anim = createSpringAnimation(mProgress, targetProgress);
        anim.setDuration(config.duration);
        anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
        anim.addListener(getProgressAnimatorListener());

        builder.play(anim);

        setAlphas(toState, config, builder);
    }

    public Animator createSpringAnimation(float... progressValues) {
        return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
                SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
    }

    private void setAlphas(LauncherState toState, AnimationConfig config,
            AnimatorSetBuilder builder) {
        setAlphas(toState.getVisibleElements(mLauncher), config, builder);
    }

    public void setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder) {
        PropertySetter setter = config == null ? NO_ANIM_PROPERTY_SETTER
                : config.getPropertySetter(builder);
        boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0;
        boolean hasContent = (visibleElements & ALL_APPS_CONTENT) != 0;

        Interpolator allAppsFade = builder.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
        setter.setViewAlpha(mAppsView.getContentView(), hasContent ? 1 : 0, allAppsFade);
        setter.setViewAlpha(mAppsView.getScrollBar(), hasContent ? 1 : 0, allAppsFade);
        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasContent, setter,
                allAppsFade);
        mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);

        setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
                (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade);
    }

    public AnimatorListenerAdapter getProgressAnimatorListener() {
        return new AnimationSuccessListener() {
            @Override
            public void onAnimationSuccess(Animator animator) {
                onProgressAnimationEnd();
            }
        };
    }

    public void setupViews(AllAppsContainerView appsView) {
        mAppsView = appsView;
        mScrimView = mLauncher.findViewById(R.id.scrim_view);
    }

    /**
     * Updates the total scroll range but does not update the UI.
     */
    void setScrollRangeDelta(float delta) {
        mScrollRangeDelta = delta;
        mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta;

        if (mScrimView != null) {
            mScrimView.reInitUi();
        }
    }

    /**
     * Set the final view states based on the progress.
     * TODO: This logic should go in {@link LauncherState}
     */
    private void onProgressAnimationEnd() {
        if (Float.compare(mProgress, 1f) == 0) {
            mAppsView.reset(false /* animate */);
        } else if (isAllAppsExpanded()) {
            mAppsView.onScrollUpEnd();
        }
    }

    private boolean isAllAppsExpanded() {
        return Float.compare(mProgress, 0f) == 0;
    }

    public void highlightWorkTabIfNecessary() {
        if (isAllAppsExpanded()) {
            mAppsView.highlightWorkTabIfNecessary();
        }
    }
}
