/*
 * 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.systemui.screenshot;

import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;

import androidx.constraintlayout.widget.ConstraintLayout;

import com.android.systemui.res.R;

/**
 * ConstraintLayout that is draggable when touched in a specific region
 */
public class DraggableConstraintLayout extends ConstraintLayout
        implements ViewTreeObserver.OnComputeInternalInsetsListener {
    public static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe

    private static final float VELOCITY_DP_PER_MS = 1;
    private static final int MAXIMUM_DISMISS_DISTANCE_DP = 400;

    private final SwipeDismissHandler mSwipeDismissHandler;
    private final GestureDetector mSwipeDetector;
    private View mActionsContainer;
    private SwipeDismissCallbacks mCallbacks;
    private final DisplayMetrics mDisplayMetrics;

    /**
     * Stores the callbacks when the view is interacted with or dismissed.
     */
    public interface SwipeDismissCallbacks {
        /**
         * Run when the view is interacted with (touched)
         */
        default void onInteraction() {

        }

        /**
         * Run when the view is dismissed (the distance threshold is met), pre-dismissal animation
         */
        default void onSwipeDismissInitiated(Animator animator) {

        }

        /**
         * Run when the view is dismissed (the distance threshold is met), post-dismissal animation
         */
        default void onDismissComplete() {

        }
    }

    public DraggableConstraintLayout(Context context) {
        this(context, null);
    }

    public DraggableConstraintLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DraggableConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mDisplayMetrics = new DisplayMetrics();
        mContext.getDisplay().getRealMetrics(mDisplayMetrics);

        mSwipeDismissHandler = new SwipeDismissHandler(mContext, this);
        setOnTouchListener(mSwipeDismissHandler);

        mSwipeDetector = new GestureDetector(mContext,
                new GestureDetector.SimpleOnGestureListener() {
                    final Rect mActionsRect = new Rect();

                    @Override
                    public boolean onScroll(
                            MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
                        mActionsContainer.getBoundsOnScreen(mActionsRect);
                        // return true if we aren't in the actions bar, or if we are but it isn't
                        // scrollable in the direction of movement
                        return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
                                || !mActionsContainer.canScrollHorizontally((int) distanceX);
                    }
                });
        mSwipeDetector.setIsLongpressEnabled(false);

        mCallbacks = new SwipeDismissCallbacks() {
        }; // default to unimplemented callbacks
    }

    public void setCallbacks(SwipeDismissCallbacks callbacks) {
        mCallbacks = callbacks;
    }

    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        mCallbacks.onInteraction();
        return super.onInterceptHoverEvent(event);
    }

    @Override // View
    protected void onFinishInflate() {
        mActionsContainer = findViewById(R.id.actions_container);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mSwipeDismissHandler.onTouch(this, ev);
        }
        return mSwipeDetector.onTouchEvent(ev);
    }

    /**
     * Cancel current dismissal animation, if any
     */
    public void cancelDismissal() {
        mSwipeDismissHandler.cancel();
    }

    /**
     * Return whether the view is currently dismissing
     */
    public boolean isDismissing() {
        return mSwipeDismissHandler.isDismissing();
    }

    /**
     * Dismiss the view, with animation controlled by SwipeDismissHandler
     */
    public void dismiss() {
        mSwipeDismissHandler.dismiss();
    }


    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
    }

    @Override
    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
        // Only child views are touchable.
        Region r = new Region();
        Rect rect = new Rect();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.VISIBLE) {
                child.getGlobalVisibleRect(rect);
                rect.inset((int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
                        (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
                r.op(rect, Region.Op.UNION);
            }
        }
        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
        inoutInfo.touchableRegion.set(r);
    }

    private int getBackgroundRight() {
        // background expected to be null in testing.
        // animation may have unexpected behavior if view is not present
        View background = findViewById(R.id.actions_container_background);
        return background == null ? 0 : background.getRight();
    }

    /**
     * Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not
     * met
     */
    private class SwipeDismissHandler implements OnTouchListener {
        private static final String TAG = "SwipeDismissHandler";

        // distance needed to register a dismissal
        private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;

        private final DraggableConstraintLayout mView;
        private final GestureDetector mGestureDetector;
        private final DisplayMetrics mDisplayMetrics;
        private ValueAnimator mDismissAnimation;

        private float mStartX;
        // Keeps track of the most recent direction (between the last two move events).
        // -1 for left; +1 for right.
        private int mDirectionX;
        private float mPreviousX;

        SwipeDismissHandler(Context context, DraggableConstraintLayout view) {
            mView = view;
            GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
            mGestureDetector = new GestureDetector(context, gestureListener);
            mDisplayMetrics = new DisplayMetrics();
            context.getDisplay().getRealMetrics(mDisplayMetrics);
        }

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            boolean gestureResult = mGestureDetector.onTouchEvent(event);
            mCallbacks.onInteraction();
            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                mStartX = event.getRawX();
                mPreviousX = mStartX;
                return true;
            } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
                    return true;
                }
                if (isPastDismissThreshold()) {
                    ValueAnimator anim = createSwipeDismissAnimation();
                    mCallbacks.onSwipeDismissInitiated(anim);
                    dismiss(anim);
                } else {
                    // if we've moved, but not past the threshold, start the return animation
                    if (DEBUG_DISMISS) {
                        Log.d(TAG, "swipe gesture abandoned");
                    }
                    createSwipeReturnAnimation().start();
                }
                return true;
            }
            return gestureResult;
        }

        class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
            @Override
            public boolean onScroll(
                    MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
                mView.setTranslationX(ev2.getRawX() - mStartX);
                mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
                mPreviousX = ev2.getRawX();
                return true;
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                    float velocityY) {
                if (mView.getTranslationX() * velocityX > 0
                        && (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
                    ValueAnimator dismissAnimator =
                            createSwipeDismissAnimation(velocityX / (float) 1000);
                    mCallbacks.onSwipeDismissInitiated(dismissAnimator);
                    dismiss(dismissAnimator);
                    return true;
                }
                return false;
            }
        }

        private boolean isPastDismissThreshold() {
            float translationX = mView.getTranslationX();
            // Determines whether the absolute translation from the start is in the same direction
            // as the current movement. For example, if the user moves most of the way to the right,
            // but then starts dragging back left, we do not dismiss even though the absolute
            // distance is greater than the threshold.
            if (translationX * mDirectionX > 0) {
                return Math.abs(translationX) >= FloatingWindowUtil.dpToPx(mDisplayMetrics,
                        DISMISS_DISTANCE_THRESHOLD_DP);
            }
            return false;
        }

        boolean isDismissing() {
            return (mDismissAnimation != null && mDismissAnimation.isRunning());
        }

        void cancel() {
            if (isDismissing()) {
                if (DEBUG_ANIM) {
                    Log.d(TAG, "cancelling dismiss animation");
                }
                mDismissAnimation.cancel();
            }
        }

        void dismiss() {
            dismiss(createSwipeDismissAnimation());
        }

        private void dismiss(ValueAnimator animator) {
            mDismissAnimation = animator;
            mDismissAnimation.addListener(new AnimatorListenerAdapter() {
                private boolean mCancelled;

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

                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    if (!mCancelled) {
                        mCallbacks.onDismissComplete();
                    }
                }
            });
            mDismissAnimation.start();
        }

        private ValueAnimator createSwipeDismissAnimation() {
            float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS);
            return createSwipeDismissAnimation(velocityPxPerMs);
        }

        private ValueAnimator createSwipeDismissAnimation(float velocity) {
            // velocity is measured in pixels per millisecond
            velocity = Math.min(3, Math.max(1, velocity));
            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
            float startX = mView.getTranslationX();
            // make sure the UI gets all the way off the screen in the direction of movement
            // (the actions container background is guaranteed to be both the leftmost and
            // rightmost UI element in LTR and RTL)
            float finalX;
            int layoutDir =
                    mView.getContext().getResources().getConfiguration().getLayoutDirection();
            if (startX > 0 || (startX == 0 && layoutDir == LAYOUT_DIRECTION_RTL)) {
                finalX = mDisplayMetrics.widthPixels;
            } else {
                finalX = -1 * getBackgroundRight();
            }
            float distance = Math.min(Math.abs(finalX - startX),
                    FloatingWindowUtil.dpToPx(mDisplayMetrics, MAXIMUM_DISMISS_DISTANCE_DP));
            // ensure that view dismisses in the right direction (right in LTR, left in RTL)
            float distanceVector = Math.copySign(distance, finalX - startX);

            anim.addUpdateListener(animation -> {
                float translation = MathUtils.lerp(
                        startX, startX + distanceVector, animation.getAnimatedFraction());
                mView.setTranslationX(translation);
                mView.setAlpha(1 - animation.getAnimatedFraction());
            });
            anim.setDuration((long) (Math.abs(distance / velocity)));
            return anim;
        }

        private ValueAnimator createSwipeReturnAnimation() {
            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
            float startX = mView.getTranslationX();
            float finalX = 0;

            anim.addUpdateListener(animation -> {
                float translation = MathUtils.lerp(
                        startX, finalX, animation.getAnimatedFraction());
                mView.setTranslationX(translation);
            });

            return anim;
        }
    }
}
