/*
 * Copyright (C) 2018 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.uioverrides.touchcontrollers;

import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;

import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.FlingBlockCheck;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.util.VibratorWrapper;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;

/**
 * Touch controller for handling task view card swipes
 */
public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
        extends AnimatorListenerAdapter implements TouchController,
        SingleAxisSwipeDetector.Listener {

    private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
    private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
    private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600;

    public static final int TASK_DISMISS_VIBRATION_PRIMITIVE =
            Utilities.ATLEAST_R ? VibrationEffect.Composition.PRIMITIVE_TICK : -1;
    public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f;
    public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK =
            VibratorWrapper.EFFECT_TEXTURE_TICK;

    protected final T mActivity;
    private final SingleAxisSwipeDetector mDetector;
    private final RecentsView mRecentsView;
    private final int[] mTempCords = new int[2];
    private final boolean mIsRtl;

    private AnimatorPlaybackController mCurrentAnimation;
    private boolean mCurrentAnimationIsGoingUp;
    private boolean mAllowGoingUp;
    private boolean mAllowGoingDown;

    private boolean mNoIntercept;

    private float mDisplacementShift;
    private float mProgressMultiplier;
    private float mEndDisplacement;
    private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
    private Float mOverrideVelocity = null;

    private TaskView mTaskBeingDragged;

    private boolean mIsDismissHapticRunning = false;

    public TaskViewTouchController(T activity) {
        mActivity = activity;
        mRecentsView = activity.getOverviewPanel();
        mIsRtl = Utilities.isRtl(activity.getResources());
        SingleAxisSwipeDetector.Direction dir =
                mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
        mDetector = new SingleAxisSwipeDetector(activity, this, dir);
    }

    private boolean canInterceptTouch(MotionEvent ev) {
        if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) {
            // Don't intercept swipes on the nav bar, as user might be trying to go home
            // during a task dismiss animation.
            if (mCurrentAnimation != null) {
                mCurrentAnimation.getAnimationPlayer().end();
            }
            return false;
        }
        if (mCurrentAnimation != null) {
            mCurrentAnimation.forceFinishIfCloseToEnd();
        }
        if (mCurrentAnimation != null) {
            // If we are already animating from a previous state, we can intercept.
            return true;
        }
        if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
            return false;
        }
        return isRecentsInteractive();
    }

    protected abstract boolean isRecentsInteractive();

    /** Is recents view showing a single task in a modal way. */
    protected abstract boolean isRecentsModal();

    protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
            clearState();
        }
    }

    @Override
    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
                && mCurrentAnimation == null) {
            clearState();
        }
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            mNoIntercept = !canInterceptTouch(ev);
            if (mNoIntercept) {
                return false;
            }

            // Now figure out which direction scroll events the controller will start
            // calling the callbacks.
            int directionsToDetectScroll = 0;
            boolean ignoreSlopWhenSettling = false;
            if (mCurrentAnimation != null) {
                directionsToDetectScroll = DIRECTION_BOTH;
                ignoreSlopWhenSettling = true;
            } else {
                mTaskBeingDragged = null;

                for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
                    TaskView view = mRecentsView.getTaskViewAt(i);

                    if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
                            .isEventOverView(view, ev)) {
                        // Disable swiping up and down if the task overlay is modal.
                        if (isRecentsModal()) {
                            mTaskBeingDragged = null;
                            break;
                        }
                        mTaskBeingDragged = view;
                        int upDirection = mRecentsView.getPagedOrientationHandler()
                                .getUpDirection(mIsRtl);

                        // The task can be dragged up to dismiss it
                        mAllowGoingUp = true;

                        // The task can be dragged down to open it if:
                        // - It's the current page
                        // - We support gestures to enter overview
                        // - It's the focused task if in grid view
                        // - The task is snapped
                        mAllowGoingDown = i == mRecentsView.getCurrentPage()
                                && DisplayController.getNavigationMode(mActivity).hasGestures
                                && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
                                && mRecentsView.isTaskInExpectedScrollPosition(i);

                        directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
                        break;
                    }
                }
                if (mTaskBeingDragged == null) {
                    mNoIntercept = true;
                    return false;
                }
            }

            mDetector.setDetectableScrollConditions(
                    directionsToDetectScroll, ignoreSlopWhenSettling);
        }

        if (mNoIntercept) {
            return false;
        }

        onControllerTouchEvent(ev);
        return mDetector.isDraggingOrSettling();
    }

    @Override
    public boolean onControllerTouchEvent(MotionEvent ev) {
        return mDetector.onTouchEvent(ev);
    }

    private void reInitAnimationController(boolean goingUp) {
        if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
            // No need to init
            return;
        }
        if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
            // Trying to re-init in an unsupported direction.
            return;
        }
        if (mCurrentAnimation != null) {
            mCurrentAnimation.setPlayFraction(0);
            mCurrentAnimation.getTarget().removeListener(this);
            mCurrentAnimation.dispatchOnCancel();
        }

        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
        mCurrentAnimationIsGoingUp = goingUp;
        BaseDragLayer dl = mActivity.getDragLayer();
        final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
        long maxDuration = 2 * secondaryLayerDimension;
        int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
        int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
        // The interpolator controlling the most prominent visual movement. We use this to determine
        // whether we passed SUCCESS_TRANSITION_PROGRESS.
        final Interpolator currentInterpolator;
        PendingAnimation pa;
        if (goingUp) {
            currentInterpolator = Interpolators.LINEAR;
            pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
                    true /* animateTaskView */, true /* removeTask */, maxDuration,
                    false /* dismissingForSplitSelection*/);

            mEndDisplacement = -secondaryTaskDimension;
        } else {
            currentInterpolator = Interpolators.ZOOM_IN;
            pa = mRecentsView.createTaskLaunchAnimation(
                    mTaskBeingDragged, maxDuration, currentInterpolator);

            // Since the thumbnail is what is filling the screen, based the end displacement on it.
            View thumbnailView = mTaskBeingDragged.getThumbnail();
            mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
            dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
            mEndDisplacement = secondaryLayerDimension - mTempCords[1];
        }
        mEndDisplacement *= verticalFactor;
        mCurrentAnimation = pa.createPlaybackController();

        // Setting this interpolator doesn't affect the visual motion, but is used to determine
        // whether we successfully reached the target state in onDragEnd().
        mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
        onUserControlledAnimationCreated(mCurrentAnimation);
        mCurrentAnimation.getTarget().addListener(this);
        mCurrentAnimation.dispatchOnStart();
        mProgressMultiplier = 1 / mEndDisplacement;
    }

    @Override
    public void onDragStart(boolean start, float startDisplacement) {
        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
        if (mCurrentAnimation == null) {
            reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
            mDisplacementShift = 0;
        } else {
            mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
            mCurrentAnimation.pause();
        }
        mFlingBlockCheck.unblockFling();
        mOverrideVelocity = null;
    }

    @Override
    public boolean onDrag(float displacement) {
        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
        float totalDisplacement = displacement + mDisplacementShift;
        boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
                orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
        if (isGoingUp != mCurrentAnimationIsGoingUp) {
            reInitAnimationController(isGoingUp);
            mFlingBlockCheck.blockFling();
        } else {
            mFlingBlockCheck.onEvent();
        }

        if (isGoingUp) {
            if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
                // Halve the value when dismissing, as we are animating the drag across the full
                // length for only the first half of the progress
                mCurrentAnimation.setPlayFraction(
                        Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1));
            } else {
                // Set mOverrideVelocity to control task dismiss velocity in onDragEnd
                int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
                if (mRecentsView.showAsGrid()) {
                    if (mTaskBeingDragged.isFocusedTask()) {
                        velocityDimenId =
                                R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
                    } else {
                        velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid;
                    }
                }
                mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId);

                // Once halfway through task dismissal interpolation, switch from reversible
                // dragging-task animation to playing the remaining task translation animations
                final long now = SystemClock.uptimeMillis();
                MotionEvent upAction = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_UP, 0.0f, 0.0f, 0);
                mDetector.onTouchEvent(upAction);
                upAction.recycle();
            }
        } else {
            mCurrentAnimation.setPlayFraction(
                    Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1));
        }

        return true;
    }

    @Override
    public void onDragEnd(float velocity) {
        if (mOverrideVelocity != null) {
            velocity = mOverrideVelocity;
            mOverrideVelocity = null;
        }
        // Limit velocity, as very large scalar values make animations play too quickly
        float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension(
                R.dimen.max_task_dismiss_drag_velocity);
        velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity,
                maxTaskDismissDragVelocity);
        boolean fling = mDetector.isFling(velocity);
        final boolean goingToEnd;
        boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
        if (blockedFling) {
            fling = false;
        }
        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
        boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
        float progress = mCurrentAnimation.getProgressFraction();
        float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
        if (fling) {
            goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
        } else {
            goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
        }
        long animationDuration = BaseSwipeDetector.calculateDuration(
                velocity, goingToEnd ? (1 - progress) : progress);
        if (blockedFling && !goingToEnd) {
            animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
        }
        // Due to very high or low velocity dismissals, animation durations can be inconsistently
        // long or short. Bound the duration for animation of task translations for a more
        // standardized feel.
        animationDuration = Utilities.boundToRange(animationDuration,
                MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION);

        mCurrentAnimation.setEndAction(this::clearState);
        mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
                velocity * orientationHandler.getSecondaryTranslationDirectionFactor(),
                mEndDisplacement, animationDuration);
        if (goingUp && goingToEnd && !mIsDismissHapticRunning) {
            VibratorWrapper.INSTANCE.get(mActivity).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE,
                    TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK);
            mIsDismissHapticRunning = true;
        }
    }

    private void clearState() {
        mDetector.finishedScrolling();
        mDetector.setDetectableScrollConditions(0, false);
        mTaskBeingDragged = null;
        mCurrentAnimation = null;
        mIsDismissHapticRunning = false;
    }
}
