/*
 * Copyright (C) 2015 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.messaging.ui.conversationlist;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.Resources;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import com.android.messaging.R;
import com.android.messaging.util.Assert;
import com.android.messaging.util.UiUtils;

/**
 * Animation and touch helper class for Conversation List swipe.
 */
public class ConversationListSwipeHelper implements OnItemTouchListener {
    private static final int UNIT_SECONDS = 1000;
    private static final boolean ANIMATING = true;

    private static final float ERROR_FACTOR_MULTIPLIER = 1.2f;
    private static final float PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.4f;
    private static final float FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.05f;

    private static final int SWIPE_DIRECTION_NONE = 0;
    private static final int SWIPE_DIRECTION_LEFT = 1;
    private static final int SWIPE_DIRECTION_RIGHT = 2;

    private final RecyclerView mRecyclerView;
    private final long mDefaultRestoreAnimationDuration;
    private final long mDefaultDismissAnimationDuration;
    private final long mMaxTranslationAnimationDuration;
    private final int mTouchSlop;
    private final int mMinimumFlingVelocity;
    private final int mMaximumFlingVelocity;

    /* Valid throughout a single gesture. */
    private VelocityTracker mVelocityTracker;
    private float mInitialX;
    private float mInitialY;
    private boolean mIsSwiping;
    private ConversationListItemView mListItemView;

    public ConversationListSwipeHelper(final RecyclerView recyclerView) {
        mRecyclerView = recyclerView;

        final Context context = mRecyclerView.getContext();
        final Resources res = context.getResources();
        mDefaultRestoreAnimationDuration = res.getInteger(R.integer.swipe_duration_ms);
        mDefaultDismissAnimationDuration = res.getInteger(R.integer.swipe_duration_ms);
        mMaxTranslationAnimationDuration = res.getInteger(R.integer.swipe_duration_ms);

        final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
        mMaximumFlingVelocity = Math.min(
                viewConfiguration.getScaledMaximumFlingVelocity(),
                res.getInteger(R.integer.swipe_max_fling_velocity_px_per_s));
        mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
    }

    @Override
    public boolean onInterceptTouchEvent(final RecyclerView recyclerView, final MotionEvent event) {
        if (event.getPointerCount() > 1) {
            // Ignore subsequent pointers.
            return false;
        }

        // We are not yet tracking a swipe gesture. Begin detection by spying on
        // touch events bubbling down to our children.
        final int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (!hasGestureSwipeTarget()) {
                    onGestureStart();

                    mVelocityTracker.addMovement(event);
                    mInitialX = event.getX();
                    mInitialY = event.getY();

                    final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY);
                    final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
                    if (viewAtPoint instanceof ConversationListItemView &&
                            child != null && child.isSwipeAnimatable()) {
                        // Begin detecting swipe on the target for the rest of the gesture.
                        mListItemView = child;
                        if (mListItemView.isAnimating()) {
                            mListItemView = null;
                        }
                    } else {
                        mListItemView = null;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (hasValidGestureSwipeTarget()) {
                    mVelocityTracker.addMovement(event);

                    final int historicalCount = event.getHistorySize();
                    // First consume the historical events, then consume the current ones.
                    for (int i = 0; i < historicalCount + 1; i++) {
                        float currX;
                        float currY;
                        if (i < historicalCount) {
                            currX = event.getHistoricalX(i);
                            currY = event.getHistoricalY(i);
                        } else {
                            currX = event.getX();
                            currY = event.getY();
                        }
                        final float deltaX = currX - mInitialX;
                        final float deltaY = currY - mInitialY;
                        final float absDeltaX = Math.abs(deltaX);
                        final float absDeltaY = Math.abs(deltaY);

                        if (!mIsSwiping && absDeltaY > mTouchSlop
                                && absDeltaY > (ERROR_FACTOR_MULTIPLIER * absDeltaX)) {
                            // Stop detecting swipe for the remainder of this gesture.
                            onGestureEnd();
                            return false;
                        }

                        if (absDeltaX > mTouchSlop) {
                            // Swipe detected. Return true so we can handle the gesture in
                            // onTouchEvent.
                            mIsSwiping = true;

                            // We don't want to suddenly jump the slop distance.
                            mInitialX = event.getX();
                            mInitialY = event.getY();

                            onSwipeGestureStart(mListItemView);
                            return true;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (hasGestureSwipeTarget()) {
                    onGestureEnd();
                }
                break;
        }

        // Start intercepting touch events from children if we detect a swipe.
        return mIsSwiping;
    }

    @Override
    public void onTouchEvent(final RecyclerView recyclerView, final MotionEvent event) {
        // We should only be here if we intercepted the touch due to swipe.
        Assert.isTrue(mIsSwiping);

        // We are now tracking a swipe gesture.
        mVelocityTracker.addMovement(event);

        final int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_MOVE:
                if (hasValidGestureSwipeTarget()) {
                    mListItemView.setSwipeTranslationX(event.getX() - mInitialX);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (hasValidGestureSwipeTarget()) {
                    final float maxVelocity = mMaximumFlingVelocity;
                    mVelocityTracker.computeCurrentVelocity(UNIT_SECONDS, maxVelocity);
                    final float velocityX = getLastComputedXVelocity();

                    final float translationX = mListItemView.getSwipeTranslationX();

                    int swipeDirection = SWIPE_DIRECTION_NONE;
                    if (translationX != 0) {
                        swipeDirection =
                                translationX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT;
                    } else if (velocityX != 0) {
                        swipeDirection =
                                velocityX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT;
                    }

                    final boolean fastEnough = isTargetSwipedFastEnough();
                    final boolean farEnough = isTargetSwipedFarEnough();

                    final boolean shouldDismiss =  (fastEnough || farEnough);

                    if (shouldDismiss) {
                        if (fastEnough) {
                            animateDismiss(mListItemView, velocityX);
                        } else {
                            animateDismiss(mListItemView, swipeDirection);
                        }
                    } else {
                        animateRestore(mListItemView, velocityX);
                    }

                    onSwipeGestureEnd(mListItemView,
                            shouldDismiss ? swipeDirection : SWIPE_DIRECTION_NONE);
                } else {
                    onGestureEnd();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (hasValidGestureSwipeTarget()) {
                    animateRestore(mListItemView, 0f);
                    onSwipeGestureEnd(mListItemView, SWIPE_DIRECTION_NONE);
                } else {
                    onGestureEnd();
                }
                break;
        }
    }


    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    }

    /**
     * We have started to intercept a series of touch events.
     */
    private void onGestureStart() {
        mIsSwiping = false;
        // Work around bug in RecyclerView that sends two identical ACTION_DOWN
        // events to #onInterceptTouchEvent.
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.clear();
    }

    /**
     * The series of touch events has been detected as a swipe.
     *
     * Now that the gesture is a swipe, we will begin translating the view of the
     * given viewHolder.
     */
    private void onSwipeGestureStart(final ConversationListItemView itemView) {
        mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
        setHardwareAnimatingLayerType(itemView, ANIMATING);
        itemView.setAnimating(true);
    }

    /**
     * The current swipe gesture is complete.
     */
    private void onSwipeGestureEnd(final ConversationListItemView itemView,
            final int swipeDirection) {
        if (swipeDirection == SWIPE_DIRECTION_RIGHT || swipeDirection == SWIPE_DIRECTION_LEFT) {
            itemView.onSwipeComplete();
        }

        // Balances out onSwipeGestureStart.
        itemView.setAnimating(false);

        onGestureEnd();
    }

    /**
     * The series of touch events has ended in an {@link MotionEvent#ACTION_UP}
     * or {@link MotionEvent#ACTION_CANCEL}.
     */
    private void onGestureEnd() {
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        mIsSwiping = false;
        mListItemView = null;
    }

    /**
     * A swipe animation has started.
     */
    private void onSwipeAnimationStart(final ConversationListItemView itemView) {
        // Disallow interactions.
        itemView.setAnimating(true);
        ViewCompat.setHasTransientState(itemView, true);
        setHardwareAnimatingLayerType(itemView, ANIMATING);
    }

    /**
     * The swipe animation has ended.
     */
    private void onSwipeAnimationEnd(final ConversationListItemView itemView) {
        // Restore interactions.
        itemView.setAnimating(false);
        ViewCompat.setHasTransientState(itemView, false);
        setHardwareAnimatingLayerType(itemView, !ANIMATING);
    }

    /**
     * Animate the dismissal of the given item. The given velocityX is taken into consideration for
     * the animation duration. Whether the item is dismissed to the left or right is dependent on
     * the given velocityX.
     */
    private void animateDismiss(final ConversationListItemView itemView, final float velocityX) {
        Assert.isTrue(velocityX != 0);
        final int direction = velocityX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT;
        animateDismiss(itemView, direction, velocityX);
    }

    /**
     * Animate the dismissal of the given item. The velocityX is assumed to be 0.
     */
    private void animateDismiss(final ConversationListItemView itemView, final int swipeDirection) {
        animateDismiss(itemView, swipeDirection, 0f);
    }

    /**
     * Animate the dismissal of the given item.
     */
    private void animateDismiss(final ConversationListItemView itemView,
            final int swipeDirection, final float velocityX) {
        Assert.isTrue(swipeDirection != SWIPE_DIRECTION_NONE);

        onSwipeAnimationStart(itemView);

        final float animateTo = (swipeDirection == SWIPE_DIRECTION_RIGHT) ?
                mRecyclerView.getWidth() : -mRecyclerView.getWidth();
        final long duration;
        if (velocityX != 0) {
            final float deltaX = animateTo - itemView.getSwipeTranslationX();
            duration = calculateTranslationDuration(deltaX, velocityX);
        } else {
            duration = mDefaultDismissAnimationDuration;
        }

        final ObjectAnimator animator = getSwipeTranslationXAnimator(
                itemView, animateTo, duration, UiUtils.DEFAULT_INTERPOLATOR);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(final Animator animation) {
                onSwipeAnimationEnd(itemView);
            }
        });
        animator.start();
    }

    /**
     * Animate the bounce back of the given item.
     */
    private void animateRestore(final ConversationListItemView itemView,
            final float velocityX) {
        onSwipeAnimationStart(itemView);

        final float translationX = itemView.getSwipeTranslationX();
        final long duration;
        if (velocityX != 0 // Has velocity.
                && velocityX > 0 != translationX > 0) { // Right direction.
            duration = calculateTranslationDuration(translationX, velocityX);
        } else {
            duration = mDefaultRestoreAnimationDuration;
        }
        final ObjectAnimator animator = getSwipeTranslationXAnimator(
                itemView, 0f, duration, UiUtils.DEFAULT_INTERPOLATOR);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(final Animator animation) {
                       onSwipeAnimationEnd(itemView);
            }
        });
        animator.start();
    }

    /**
     * Create and start an animator that animates the given view's translationX
     * from its current value to the value given by animateTo.
     */
    private ObjectAnimator getSwipeTranslationXAnimator(final ConversationListItemView itemView,
            final float animateTo, final long duration, final TimeInterpolator interpolator) {
        final ObjectAnimator animator =
                ObjectAnimator.ofFloat(itemView, "swipeTranslationX", animateTo);
        animator.setDuration(duration);
        animator.setInterpolator(interpolator);
        return animator;
    }

    /**
     * Determine if the swipe has enough velocity to be dismissed.
     */
    private boolean isTargetSwipedFastEnough() {
        final float velocityX = getLastComputedXVelocity();
        final float velocityY = mVelocityTracker.getYVelocity();
        final float minVelocity = mMinimumFlingVelocity;
        final float translationX = mListItemView.getSwipeTranslationX();
        final float width = mListItemView.getWidth();
        return (Math.abs(velocityX) > minVelocity) // Fast enough.
                && (Math.abs(velocityX) > Math.abs(velocityY)) // Not unintentional.
                && (velocityX > 0) == (translationX > 0) // Right direction.
                && Math.abs(translationX) >
                    FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS * width; // Enough movement.
  }

    /**
     * Only used during a swipe gesture. Determine if the swipe has enough distance to be
     * dismissed.
     */
    private boolean isTargetSwipedFarEnough() {
        final float velocityX = getLastComputedXVelocity();

        final float translationX = mListItemView.getSwipeTranslationX();
        final float width = mListItemView.getWidth();

        return (velocityX >= 0) == (translationX > 0) // Right direction.
                && Math.abs(translationX) >
                    PERCENTAGE_OF_WIDTH_TO_DISMISS * width; // Enough movement.
  }

    private long calculateTranslationDuration(final float deltaPosition, final float velocity) {
        Assert.isTrue(velocity != 0);
        final float durationInSeconds = Math.abs(deltaPosition / velocity);
        return Math.min((int) (durationInSeconds * UNIT_SECONDS), mMaxTranslationAnimationDuration);
    }

    private boolean hasGestureSwipeTarget() {
        return mListItemView != null;
    }

    private boolean hasValidGestureSwipeTarget() {
        return hasGestureSwipeTarget() && mListItemView.getParent() == mRecyclerView;
    }

    /**
     * Enable a hardware layer for the it view and build that layer.
     */
    private void setHardwareAnimatingLayerType(final ConversationListItemView itemView,
            final boolean animating) {
        if (animating) {
            itemView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            if (itemView.getWindowToken() != null) {
                itemView.buildLayer();
            }
        } else {
            itemView.setLayerType(View.LAYER_TYPE_NONE, null);
        }
    }

    private float getLastComputedXVelocity() {
        return mVelocityTracker.getXVelocity();
    }
}