/*
 * Copyright (C) 2022 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 android.window;

import android.annotation.FloatRange;
import android.os.SystemProperties;
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;

import java.io.PrintWriter;

/**
 * Helper class to record the touch location for gesture and generate back events.
 * @hide
 */
public class BackTouchTracker {
    private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
            "persist.wm.debug.predictive_back_linear_distance";
    private static final int LINEAR_DISTANCE = SystemProperties
            .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
    private float mLinearDistance = LINEAR_DISTANCE;
    private float mMaxDistance;
    private float mNonLinearFactor;
    /**
     * Location of the latest touch event
     */
    private float mLatestTouchX;
    private float mLatestTouchY;
    private boolean mTriggerBack;

    /**
     * Location of the initial touch event of the back gesture.
     */
    private float mInitTouchX;
    private float mInitTouchY;
    private float mStartThresholdX;
    private int mSwipeEdge;
    private boolean mShouldUpdateStartLocation = false;
    private TouchTrackerState mState = TouchTrackerState.INITIAL;

    /**
     * Updates the tracker with a new motion event.
     */
    public void update(float touchX, float touchY) {
        /**
         * If back was previously cancelled but the user has started swiping in the forward
         * direction again, restart back.
         */
        if ((touchX < mStartThresholdX && mSwipeEdge == BackEvent.EDGE_LEFT)
                || (touchX > mStartThresholdX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
            mStartThresholdX = touchX;
            if ((mSwipeEdge == BackEvent.EDGE_LEFT && mStartThresholdX < mInitTouchX)
                    || (mSwipeEdge == BackEvent.EDGE_RIGHT && mStartThresholdX > mInitTouchX)) {
                mInitTouchX = mStartThresholdX;
            }
        }
        mLatestTouchX = touchX;
        mLatestTouchY = touchY;
    }

    /** Sets whether the back gesture is past the trigger threshold. */
    public void setTriggerBack(boolean triggerBack) {
        if (mTriggerBack != triggerBack && !triggerBack) {
            mStartThresholdX = mLatestTouchX;
        }
        mTriggerBack = triggerBack;
    }

    /** Gets whether the back gesture is past the trigger threshold. */
    public boolean getTriggerBack() {
        return mTriggerBack;
    }


    /** Returns if the start location should be updated. */
    public boolean shouldUpdateStartLocation() {
        return mShouldUpdateStartLocation;
    }

    /** Sets if the start location should be updated. */
    public void setShouldUpdateStartLocation(boolean shouldUpdate) {
        mShouldUpdateStartLocation = shouldUpdate;
    }

    /** Sets the state of the touch tracker. */
    public void setState(TouchTrackerState state) {
        mState = state;
    }

    /** Returns if the tracker is in initial state. */
    public boolean isInInitialState() {
        return mState == TouchTrackerState.INITIAL;
    }

    /** Returns if a back gesture is active. */
    public boolean isActive() {
        return mState == TouchTrackerState.ACTIVE;
    }

    /** Returns if a back gesture has been finished. */
    public boolean isFinished() {
        return mState == TouchTrackerState.FINISHED;
    }

    /** Sets the start location of the back gesture. */
    public void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
        mInitTouchX = touchX;
        mInitTouchY = touchY;
        mLatestTouchX = touchX;
        mLatestTouchY = touchY;
        mSwipeEdge = swipeEdge;
        mStartThresholdX = mInitTouchX;
    }

    /** Update the start location used to compute the progress to the latest touch location. */
    public void updateStartLocation() {
        mInitTouchX = mLatestTouchX;
        mInitTouchY = mLatestTouchY;
        mStartThresholdX = mInitTouchX;
        mShouldUpdateStartLocation = false;
    }

    /** Resets the tracker. */
    public void reset() {
        mInitTouchX = 0;
        mInitTouchY = 0;
        mStartThresholdX = 0;
        mTriggerBack = false;
        mState = TouchTrackerState.INITIAL;
        mSwipeEdge = BackEvent.EDGE_LEFT;
        mShouldUpdateStartLocation = false;
    }

    /** Creates a start {@link BackMotionEvent}. */
    public BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
        return new BackMotionEvent(
                /* touchX = */ mInitTouchX,
                /* touchY = */ mInitTouchY,
                /* frameTimeMillis = */ 0,
                /* progress = */ 0,
                /* triggerBack = */ mTriggerBack,
                /* swipeEdge = */ mSwipeEdge,
                /* departingAnimationTarget = */ target);
    }

    /** Creates a progress {@link BackMotionEvent}. */
    public BackMotionEvent createProgressEvent() {
        float progress = getProgress(mLatestTouchX);
        return createProgressEvent(progress);
    }

    /**
     * Progress value computed from the touch position.
     *
     * @param touchX the X touch position of the {@link MotionEvent}.
     * @return progress value
     */
    @FloatRange(from = 0.0, to = 1.0)
    public float getProgress(float touchX) {
        // If back is committed, progress is the distance between the last and first touch
        // point, divided by the max drag distance. Otherwise, it's the distance between
        // the last touch point and the starting threshold, divided by max drag distance.
        // The starting threshold is initially the first touch location, and updated to
        // the location everytime back is restarted after being cancelled.
        float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
        float distance;
        if (mSwipeEdge == BackEvent.EDGE_LEFT) {
            distance = touchX - startX;
        } else {
            distance = startX - touchX;
        }
        float deltaX = Math.max(0f, distance);
        float linearDistance = mLinearDistance;
        float maxDistance = getMaxDistance();
        maxDistance = maxDistance == 0 ? 1 : maxDistance;
        float progress;
        if (linearDistance < maxDistance) {
            // Up to linearDistance it behaves linearly, then slowly reaches 1f.

            // maxDistance is composed of linearDistance + nonLinearDistance
            float nonLinearDistance = maxDistance - linearDistance;
            float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;

            boolean isLinear = deltaX <= linearDistance;
            if (isLinear) {
                progress = deltaX / initialTarget;
            } else {
                float nonLinearDeltaX = deltaX - linearDistance;
                float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
                float currentTarget = MathUtils.lerp(
                        /* start = */ initialTarget,
                        /* stop = */ maxDistance,
                        /* amount = */ nonLinearProgress);
                progress = deltaX / currentTarget;
            }
        } else {
            // Always linear behavior.
            progress = deltaX / maxDistance;
        }
        return MathUtils.constrain(progress, 0, 1);
    }

    /**
     * Maximum distance in pixels.
     * Progress is considered to be completed (1f) when this limit is exceeded.
     */
    public float getMaxDistance() {
        return mMaxDistance;
    }

    public float getLinearDistance() {
        return mLinearDistance;
    }

    public float getNonLinearFactor() {
        return mNonLinearFactor;
    }

    /** Creates a progress {@link BackMotionEvent} for the given progress. */
    public BackMotionEvent createProgressEvent(float progress) {
        return new BackMotionEvent(
                /* touchX = */ mLatestTouchX,
                /* touchY = */ mLatestTouchY,
                /* frameTimeMillis = */ 0,
                /* progress = */ progress,
                /* triggerBack = */ mTriggerBack,
                /* swipeEdge = */ mSwipeEdge,
                /* departingAnimationTarget = */ null);
    }

    /** Sets the thresholds for computing progress. */
    public void setProgressThresholds(float linearDistance, float maxDistance,
            float nonLinearFactor) {
        if (LINEAR_DISTANCE >= 0) {
            mLinearDistance = LINEAR_DISTANCE;
        } else {
            mLinearDistance = linearDistance;
        }
        mMaxDistance = maxDistance;
        mNonLinearFactor = nonLinearFactor;
    }

    /** Dumps debugging info. */
    public void dump(PrintWriter pw, String prefix) {
        pw.println(prefix + "BackTouchTracker state:");
        pw.println(prefix + "  mState=" + mState);
        pw.println(prefix + "  mTriggerBack=" + mTriggerBack);
    }

    public enum TouchTrackerState {
        INITIAL, ACTIVE, FINISHED
    }

}
