/*
 * Copyright (C) 2014 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.systemui.statusbar.phone;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.classifier.Classifier;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.statusbar.KeyguardAffordanceView;
import com.android.wm.shell.animation.FlingAnimationUtils;

/**
 * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
 */
public class KeyguardAffordanceHelper {

    public static final long HINT_PHASE1_DURATION = 200;
    private static final long HINT_PHASE2_DURATION = 350;
    private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
    private static final int HINT_CIRCLE_OPEN_DURATION = 500;

    private final Context mContext;
    private final Callback mCallback;

    private FlingAnimationUtils mFlingAnimationUtils;
    private VelocityTracker mVelocityTracker;
    private boolean mSwipingInProgress;
    private float mInitialTouchX;
    private float mInitialTouchY;
    private float mTranslation;
    private float mTranslationOnDown;
    private int mTouchSlop;
    private int mMinTranslationAmount;
    private int mMinFlingVelocity;
    private int mHintGrowAmount;
    private KeyguardAffordanceView mLeftIcon;
    private KeyguardAffordanceView mRightIcon;
    private Animator mSwipeAnimator;
    private final FalsingManager mFalsingManager;
    private int mMinBackgroundRadius;
    private boolean mMotionCancelled;
    private int mTouchTargetSize;
    private View mTargetedView;
    private boolean mTouchSlopExeeded;
    private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mSwipeAnimator = null;
            mSwipingInProgress = false;
            mTargetedView = null;
        }
    };
    private Runnable mAnimationEndRunnable = new Runnable() {
        @Override
        public void run() {
            mCallback.onAnimationToSideEnded();
        }
    };

    KeyguardAffordanceHelper(Callback callback, Context context, FalsingManager falsingManager) {
        mContext = context;
        mCallback = callback;
        initIcons();
        updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false);
        updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false);
        mFalsingManager = falsingManager;
        initDimens();
    }

    private void initDimens() {
        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
        mTouchSlop = configuration.getScaledPagingTouchSlop();
        mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
                R.dimen.keyguard_min_swipe_amount);
        mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
                R.dimen.keyguard_affordance_min_background_radius);
        mTouchTargetSize = mContext.getResources().getDimensionPixelSize(
                R.dimen.keyguard_affordance_touch_target_size);
        mHintGrowAmount =
                mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
        mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(),
                0.4f);
    }

    private void initIcons() {
        mLeftIcon = mCallback.getLeftIcon();
        mRightIcon = mCallback.getRightIcon();
        updatePreviews();
    }

    public void updatePreviews() {
        mLeftIcon.setPreviewView(mCallback.getLeftPreview());
        mRightIcon.setPreviewView(mCallback.getRightPreview());
    }

    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) {
            return false;
        }
        final float y = event.getY();
        final float x = event.getX();

        boolean isUp = false;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                View targetView = getIconAtPosition(x, y);
                if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) {
                    mMotionCancelled = true;
                    return false;
                }
                if (mTargetedView != null) {
                    cancelAnimation();
                } else {
                    mTouchSlopExeeded = false;
                }
                startSwiping(targetView);
                mInitialTouchX = x;
                mInitialTouchY = y;
                mTranslationOnDown = mTranslation;
                initVelocityTracker();
                trackMovement(event);
                mMotionCancelled = false;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                mMotionCancelled = true;
                endMotion(true /* forceSnapBack */, x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                trackMovement(event);
                float xDist = x - mInitialTouchX;
                float yDist = y - mInitialTouchY;
                float distance = (float) Math.hypot(xDist, yDist);
                if (!mTouchSlopExeeded && distance > mTouchSlop) {
                    mTouchSlopExeeded = true;
                }
                if (mSwipingInProgress) {
                    if (mTargetedView == mRightIcon) {
                        distance = mTranslationOnDown - distance;
                        distance = Math.min(0, distance);
                    } else {
                        distance = mTranslationOnDown + distance;
                        distance = Math.max(0, distance);
                    }
                    setTranslation(distance, false /* isReset */, false /* animateReset */);
                }
                break;

            case MotionEvent.ACTION_UP:
                isUp = true;
            case MotionEvent.ACTION_CANCEL:
                boolean hintOnTheRight = mTargetedView == mRightIcon;
                trackMovement(event);
                endMotion(!isUp, x, y);
                if (!mTouchSlopExeeded && isUp) {
                    mCallback.onIconClicked(hintOnTheRight);
                }
                break;
        }
        return true;
    }

    private void startSwiping(View targetView) {
        mCallback.onSwipingStarted(targetView == mRightIcon);
        mSwipingInProgress = true;
        mTargetedView = targetView;
    }

    private View getIconAtPosition(float x, float y) {
        if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) {
            return mLeftIcon;
        }
        if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) {
            return mRightIcon;
        }
        return null;
    }

    public boolean isOnAffordanceIcon(float x, float y) {
        return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y);
    }

    private boolean isOnIcon(View icon, float x, float y) {
        float iconX = icon.getX() + icon.getWidth() / 2.0f;
        float iconY = icon.getY() + icon.getHeight() / 2.0f;
        double distance = Math.hypot(x - iconX, y - iconY);
        return distance <= mTouchTargetSize / 2;
    }

    private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
        if (mSwipingInProgress) {
            flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
        } else {
            mTargetedView = null;
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private boolean rightSwipePossible() {
        return mRightIcon.getVisibility() == View.VISIBLE;
    }

    private boolean leftSwipePossible() {
        return mLeftIcon.getVisibility() == View.VISIBLE;
    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    public void startHintAnimation(boolean right,
            Runnable onFinishedListener) {
        cancelAnimation();
        startHintAnimationPhase1(right, onFinishedListener);
    }

    private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
        ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
        animator.addListener(new AnimatorListenerAdapter() {
            private boolean mCancelled;

            @Override
            public void onAnimationCancel(Animator animation) {
                mCancelled = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mCancelled) {
                    mSwipeAnimator = null;
                    mTargetedView = null;
                    onFinishedListener.run();
                } else {
                    startUnlockHintAnimationPhase2(right, onFinishedListener);
                }
            }
        });
        animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
        animator.setDuration(HINT_PHASE1_DURATION);
        animator.start();
        mSwipeAnimator = animator;
        mTargetedView = targetView;
    }

    /**
     * Phase 2: Move back.
     */
    private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
        ValueAnimator animator = getAnimatorToRadius(right, 0);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mSwipeAnimator = null;
                mTargetedView = null;
                onFinishedListener.run();
            }
        });
        animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
        animator.setDuration(HINT_PHASE2_DURATION);
        animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
        animator.start();
        mSwipeAnimator = animator;
    }

    private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
        ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float newRadius = (float) animation.getAnimatedValue();
                targetView.setCircleRadiusWithoutAnimation(newRadius);
                float translation = getTranslationFromRadius(newRadius);
                mTranslation = right ? -translation : translation;
                updateIconsFromTranslation(targetView);
            }
        });
        return animator;
    }

    private void cancelAnimation() {
        if (mSwipeAnimator != null) {
            mSwipeAnimator.cancel();
        }
    }

    private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
        float vel = getCurrentVelocity(lastX, lastY);

        // We snap back if the current translation is not far enough
        boolean snapBack = false;
        if (mCallback.needsAntiFalsing()) {
            snapBack = snapBack || mFalsingManager.isFalseTouch(
                    mTargetedView == mRightIcon
                            ? Classifier.RIGHT_AFFORDANCE : Classifier.LEFT_AFFORDANCE);
        }
        snapBack = snapBack || isBelowFalsingThreshold();

        // or if the velocity is in the opposite direction.
        boolean velIsInWrongDirection = vel * mTranslation < 0;
        snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
        vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
        fling(vel, snapBack || forceSnapBack, mTranslation < 0);
    }

    private boolean isBelowFalsingThreshold() {
        return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
    }

    private int getMinTranslationAmount() {
        float factor = mCallback.getAffordanceFalsingFactor();
        return (int) (mMinTranslationAmount * factor);
    }

    private void fling(float vel, final boolean snapBack, boolean right) {
        float target = right ? -mCallback.getMaxTranslationDistance()
                : mCallback.getMaxTranslationDistance();
        target = snapBack ? 0 : target;

        ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
        mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mTranslation = (float) animation.getAnimatedValue();
            }
        });
        animator.addListener(mFlingEndListener);
        if (!snapBack) {
            startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right);
            mCallback.onAnimationToSideStarted(right, mTranslation, vel);
        } else {
            reset(true);
        }
        animator.start();
        mSwipeAnimator = animator;
        if (snapBack) {
            mCallback.onSwipingAborted();
        }
    }

    private void startFinishingCircleAnimation(float velocity, Runnable animationEndRunnable,
            boolean right) {
        KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
        targetView.finishAnimation(velocity, animationEndRunnable);
    }

    private void setTranslation(float translation, boolean isReset, boolean animateReset) {
        translation = rightSwipePossible() ? translation : Math.max(0, translation);
        translation = leftSwipePossible() ? translation : Math.min(0, translation);
        float absTranslation = Math.abs(translation);
        if (translation != mTranslation || isReset) {
            KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
            KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
            float alpha = absTranslation / getMinTranslationAmount();

            // We interpolate the alpha of the other icons to 0
            float fadeOutAlpha = 1.0f - alpha;
            fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);

            boolean animateIcons = isReset && animateReset;
            boolean forceNoCircleAnimation = isReset && !animateReset;
            float radius = getRadiusFromTranslation(absTranslation);
            boolean slowAnimation = isReset && isBelowFalsingThreshold();
            if (!isReset) {
                updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(),
                        false, false, false, false);
            } else {
                updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(),
                        animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation);
            }
            updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(),
                    animateIcons, slowAnimation, isReset, forceNoCircleAnimation);

            mTranslation = translation;
        }
    }

    private void updateIconsFromTranslation(KeyguardAffordanceView targetView) {
        float absTranslation = Math.abs(mTranslation);
        float alpha = absTranslation / getMinTranslationAmount();

        // We interpolate the alpha of the other icons to 0
        float fadeOutAlpha =  1.0f - alpha;
        fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);

        // We interpolate the alpha of the targetView to 1
        KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
        updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
        updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
    }

    private float getTranslationFromRadius(float circleSize) {
        float translation = (circleSize - mMinBackgroundRadius)
                / BACKGROUND_RADIUS_SCALE_FACTOR;
        return translation > 0.0f ? translation + mTouchSlop : 0.0f;
    }

    private float getRadiusFromTranslation(float translation) {
        if (translation <= mTouchSlop) {
            return 0.0f;
        }
        return (translation - mTouchSlop)  * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
    }

    public void animateHideLeftRightIcon() {
        cancelAnimation();
        updateIcon(mRightIcon, 0f, 0f, true, false, false, false);
        updateIcon(mLeftIcon, 0f, 0f, true, false, false, false);
    }

    private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
                            boolean animate, boolean slowRadiusAnimation, boolean force,
                            boolean forceNoCircleAnimation) {
        if (view.getVisibility() != View.VISIBLE && !force) {
            return;
        }
        if (forceNoCircleAnimation) {
            view.setCircleRadiusWithoutAnimation(circleRadius);
        } else {
            view.setCircleRadius(circleRadius, slowRadiusAnimation);
        }
        updateIconAlpha(view, alpha, animate);
    }

    private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
        float scale = getScale(alpha, view);
        alpha = Math.min(1.0f, alpha);
        view.setImageAlpha(alpha, animate);
        view.setImageScale(scale, animate);
    }

    private float getScale(float alpha, KeyguardAffordanceView icon) {
        float scale = alpha / icon.getRestingAlpha() * 0.2f +
                KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
        return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
    }

    private void trackMovement(MotionEvent event) {
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }
    }

    private void initVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
        }
        mVelocityTracker = VelocityTracker.obtain();
    }

    private float getCurrentVelocity(float lastX, float lastY) {
        if (mVelocityTracker == null) {
            return 0;
        }
        mVelocityTracker.computeCurrentVelocity(1000);
        float aX = mVelocityTracker.getXVelocity();
        float aY = mVelocityTracker.getYVelocity();
        float bX = lastX - mInitialTouchX;
        float bY = lastY - mInitialTouchY;
        float bLen = (float) Math.hypot(bX, bY);
        // Project the velocity onto the distance vector: a * b / |b|
        float projectedVelocity = (aX * bX + aY * bY) / bLen;
        if (mTargetedView == mRightIcon) {
            projectedVelocity = -projectedVelocity;
        }
        return projectedVelocity;
    }

    public void onConfigurationChanged() {
        initDimens();
        initIcons();
    }

    public void onRtlPropertiesChanged() {
        initIcons();
    }

    public void reset(boolean animate) {
        cancelAnimation();
        setTranslation(0.0f, true /* isReset */, animate);
        mMotionCancelled = true;
        if (mSwipingInProgress) {
            mCallback.onSwipingAborted();
            mSwipingInProgress = false;
        }
    }

    public boolean isSwipingInProgress() {
        return mSwipingInProgress;
    }

    public void launchAffordance(boolean animate, boolean left) {
        if (mSwipingInProgress) {
            // We don't want to mess with the state if the user is actually swiping already.
            return;
        }
        KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon;
        KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon;
        startSwiping(targetView);

        // Do not animate the circle expanding if the affordance isn't visible,
        // otherwise the circle will be meaningless.
        if (targetView.getVisibility() != View.VISIBLE) {
            animate = false;
        }

        if (animate) {
            fling(0, false, !left);
            updateIcon(otherView, 0.0f, 0, true, false, true, false);
        } else {
            mCallback.onAnimationToSideStarted(!left, mTranslation, 0);
            mTranslation = left ? mCallback.getMaxTranslationDistance()
                    : mCallback.getMaxTranslationDistance();
            updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
            targetView.instantFinishAnimation();
            mFlingEndListener.onAnimationEnd(null);
            mAnimationEndRunnable.run();
        }
    }

    public interface Callback {

        /**
         * Notifies the callback when an animation to a side page was started.
         *
         * @param rightPage Is the page animated to the right page?
         */
        void onAnimationToSideStarted(boolean rightPage, float translation, float vel);

        /**
         * Notifies the callback the animation to a side page has ended.
         */
        void onAnimationToSideEnded();

        float getMaxTranslationDistance();

        void onSwipingStarted(boolean rightIcon);

        void onSwipingAborted();

        void onIconClicked(boolean rightIcon);

        KeyguardAffordanceView getLeftIcon();

        KeyguardAffordanceView getRightIcon();

        View getLeftPreview();

        View getRightPreview();

        /**
         * @return The factor the minimum swipe amount should be multiplied with.
         */
        float getAffordanceFalsingFactor();

        boolean needsAntiFalsing();
    }
}
