/*
 * Copyright (C) 2020 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.view;

import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.graphics.Insets;
import android.view.animation.Interpolator;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/**
 * Class representing an animation of a set of windows that cause insets.
 */
public final class WindowInsetsAnimation {

    @WindowInsets.Type.InsetsType
    private final int mTypeMask;
    private float mFraction;
    @Nullable
    private final Interpolator mInterpolator;
    private final long mDurationMillis;
    private float mAlpha;

    /**
     * Creates a new {@link WindowInsetsAnimation} object.
     * <p>
     * This should only be used for testing, as usually the system creates this object for the
     * application to listen to with {@link Callback}.
     * </p>
     * @param typeMask The bitmask of {@link WindowInsets.Type}s that are animating.
     * @param interpolator The interpolator of the animation.
     * @param durationMillis The duration of the animation in
     *                   {@link java.util.concurrent.TimeUnit#MILLISECONDS}.
     */
    public WindowInsetsAnimation(
            @WindowInsets.Type.InsetsType int typeMask, @Nullable Interpolator interpolator,
            long durationMillis) {
        mTypeMask = typeMask;
        mInterpolator = interpolator;
        mDurationMillis = durationMillis;
    }

    /**
     * @return The bitmask of {@link WindowInsets.Type}s that are animating.
     */
    @WindowInsets.Type.InsetsType
    public int getTypeMask() {
        return mTypeMask;
    }

    /**
     * Returns the raw fractional progress of this animation between
     * start state of the animation and the end state of the animation. Note
     * that this progress is the global progress of the animation, whereas
     * {@link Callback#onProgress} will only dispatch the insets that may
     * be inset with {@link WindowInsets#inset} by parents of views in the hierarchy.
     * Progress per insets animation is global for the entire animation. One animation animates
     * all things together (in, out, ...). If they don't animate together, we'd have
     * multiple animations.
     * <p>
     * Note: In case the application is controlling the animation, the valued returned here will
     * be the same as the application passed into
     * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}.
     * </p>
     * @return The current progress of this animation.
     */
    @FloatRange(from = 0f, to = 1f)
    public float getFraction() {
        return mFraction;
    }

    /**
     * Returns the interpolated fractional progress of this animation between
     * start state of the animation and the end state of the animation. Note
     * that this progress is the global progress of the animation, whereas
     * {@link Callback#onProgress} will only dispatch the insets that may
     * be inset with {@link WindowInsets#inset} by parents of views in the hierarchy.
     * Progress per insets animation is global for the entire animation. One animation animates
     * all things together (in, out, ...). If they don't animate together, we'd have
     * multiple animations.
     * <p>
     * Note: In case the application is controlling the animation, the valued returned here will
     * be the same as the application passed into
     * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)},
     * interpolated with the interpolator passed into
     * {@link WindowInsetsController#controlWindowInsetsAnimation}.
     * </p>
     * <p>
     * Note: For system-initiated animations, this will always return a valid value between 0
     * and 1.
     * </p>
     * @see #getFraction() for raw fraction.
     * @return The current interpolated progress of this animation.
     */
    public float getInterpolatedFraction() {
        if (mInterpolator != null) {
            return mInterpolator.getInterpolation(mFraction);
        }
        return mFraction;
    }

    /**
     * Retrieves the interpolator used for this animation, or {@code null} if this animation
     * doesn't follow an interpolation curved. For system-initiated animations, this will never
     * return {@code null}.
     *
     * @return The interpolator used for this animation.
     */
    @Nullable
    public Interpolator getInterpolator() {
        return mInterpolator;
    }

    /**
     * @return duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or
     *         -1 if the animation doesn't have a fixed duration.
     */
    public long getDurationMillis() {
        return mDurationMillis;
    }

    /**
     * Set fraction of the progress if {@link WindowInsets.Type} animation is
     * controlled by the app.
     * <p>
     * Note: This should only be used for testing, as the system fills in the fraction for the
     * application or the fraction that was passed into
     * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)} is being
     * used.
     * </p>
     * @param fraction fractional progress between 0 and 1 where 0 represents hidden and
     *                zero progress and 1 represent fully shown final state.
     * @see #getFraction()
     */
    public void setFraction(@FloatRange(from = 0f, to = 1f) float fraction) {
        mFraction = fraction;
    }

    /**
     * Retrieves the translucency of the windows that are animating.
     *
     * @return Alpha of windows that cause insets of type {@link WindowInsets.Type}.
     */
    @FloatRange(from = 0f, to = 1f)
    public float getAlpha() {
        return mAlpha;
    }

    /**
     * Sets the translucency of the windows that are animating.
     * <p>
     * Note: This should only be used for testing, as the system fills in the alpha for the
     * application or the alpha that was passed into
     * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)} is being
     * used.
     * </p>
     * @param alpha Alpha of windows that cause insets of type {@link WindowInsets.Type}.
     * @see #getAlpha()
     */
    public void setAlpha(@FloatRange(from = 0f, to = 1f) float alpha) {
        mAlpha = alpha;
    }

    /**
     * Class representing the range of an {@link WindowInsetsAnimation}
     */
    public static final class Bounds {

        private final Insets mLowerBound;
        private final Insets mUpperBound;

        public Bounds(@NonNull Insets lowerBound, @NonNull Insets upperBound) {
            mLowerBound = lowerBound;
            mUpperBound = upperBound;
        }

        /**
         * Queries the lower inset bound of the animation. If the animation is about showing or
         * hiding a window that cause insets, the lower bound is {@link Insets#NONE} and the upper
         * bound is the same as {@link WindowInsets#getInsets(int)} for the fully shown state. This
         * is the same as {@link WindowInsetsAnimationController#getHiddenStateInsets} and
         * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets
         * invoked because of an animation that originates from
         * {@link WindowInsetsAnimationController}.
         * <p>
         * However, if the size of a window that causes insets is changing, these are the
         * lower/upper bounds of that size animation.
         * </p>
         * There are no overlapping animations for a specific type, but there may be multiple
         * animations running at the same time for different inset types.
         *
         * @see #getUpperBound()
         * @see WindowInsetsAnimationController#getHiddenStateInsets
         */
        @NonNull
        public Insets getLowerBound() {
            return mLowerBound;
        }

        /**
         * Queries the upper inset bound of the animation. If the animation is about showing or
         * hiding a window that cause insets, the lower bound is {@link Insets#NONE}
         * nd the upper bound is the same as {@link WindowInsets#getInsets(int)} for the fully
         * shown state. This is the same as
         * {@link WindowInsetsAnimationController#getHiddenStateInsets} and
         * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets
         * invoked because of an animation that originates from
         * {@link WindowInsetsAnimationController}.
         * <p>
         * However, if the size of a window that causes insets is changing, these are the
         * lower/upper bounds of that size animation.
         * <p>
         * There are no overlapping animations for a specific type, but there may be multiple
         * animations running at the same time for different inset types.
         *
         * @see #getLowerBound()
         * @see WindowInsetsAnimationController#getShownStateInsets
         */
        @NonNull
        public Insets getUpperBound() {
            return mUpperBound;
        }

        /**
         * Insets both the lower and upper bound by the specified insets. This is to be used in
         * {@link Callback#onStart} to indicate that a part of the insets has
         * been used to offset or clip its children, and the children shouldn't worry about that
         * part anymore.
         *
         * @param insets The amount to inset.
         * @return A copy of this instance inset in the given directions.
         * @see WindowInsets#inset
         * @see Callback#onStart
         */
        @NonNull
        public Bounds inset(@NonNull Insets insets) {
            return new Bounds(
                    // TODO: refactor so that WindowInsets.insetInsets() is in a more appropriate
                    //  place eventually.
                    WindowInsets.insetInsets(
                            mLowerBound, insets.left, insets.top, insets.right, insets.bottom),
                    WindowInsets.insetInsets(
                            mUpperBound, insets.left, insets.top, insets.right, insets.bottom));
        }

        @Override
        public String toString() {
            return "Bounds{lower=" + mLowerBound + " upper=" + mUpperBound + "}";
        }
    }

    /**
     * Interface that allows the application to listen to animation events for windows that cause
     * insets.
     */
    @SuppressLint("CallbackMethodName") // TODO(b/149430296) Should be on method, not class.
    public abstract static class Callback {

        /**
         * Return value for {@link #getDispatchMode()}: Dispatching of animation events should
         * stop at this level in the view hierarchy, and no animation events should be dispatch to
         * the subtree of the view hierarchy.
         */
        public static final int DISPATCH_MODE_STOP = 0;

        /**
         * Return value for {@link #getDispatchMode()}: Dispatching of animation events should
         * continue in the view hierarchy.
         */
        public static final int DISPATCH_MODE_CONTINUE_ON_SUBTREE = 1;

        /** @hide */
        @IntDef(prefix = { "DISPATCH_MODE_" }, value = {
                DISPATCH_MODE_STOP,
                DISPATCH_MODE_CONTINUE_ON_SUBTREE
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface DispatchMode {}

        @DispatchMode
        private final int mDispatchMode;

        /**
         * Creates a new {@link WindowInsetsAnimation} callback with the given
         * {@link #getDispatchMode() dispatch mode}.
         *
         * @param dispatchMode The dispatch mode for this callback. See {@link #getDispatchMode()}.
         */
        public Callback(@DispatchMode int dispatchMode) {
            mDispatchMode = dispatchMode;
        }

        /**
         * Retrieves the dispatch mode of this listener. Dispatch of the all animation events is
         * hierarchical: It will starts at the root of the view hierarchy and then traverse it and
         * invoke the callback of the specific {@link View} that is being traversed.
         * The method may return either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that
         * animation events should be propagated to the subtree of the view hierarchy, or
         * {@link #DISPATCH_MODE_STOP} to stop dispatching. In that case, all animation callbacks
         * related to the animation passed in will be stopped from propagating to the subtree of the
         * hierarchy.
         * <p>
         * Also note that {@link #DISPATCH_MODE_STOP} behaves the same way as
         * returning {@link WindowInsets#CONSUMED} during the regular insets dispatch in
         * {@link View#onApplyWindowInsets}.
         *
         * @return Either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that dispatching of
         *         animation events will continue to the subtree of the view hierarchy, or
         *         {@link #DISPATCH_MODE_STOP} to indicate that animation events will stop
         *         dispatching.
         */
        @DispatchMode
        @SuppressLint("CallbackMethodName") // TODO(b/149430296) False positive: not a callback.
        public final int getDispatchMode() {
            return mDispatchMode;
        }

        /**
         * Called when an insets animation is about to start and before the views have been laid out
         * in the end state of the animation. The ordering of events during an insets animation is
         * the following:
         * <p>
         * <ul>
         *     <li>Application calls {@link WindowInsetsController#hide(int)},
         *     {@link WindowInsetsController#show(int)},
         *     {@link WindowInsetsController#controlWindowInsetsAnimation}</li>
         *     <li>onPrepare is called on the view hierarchy listeners</li>
         *     <li>{@link View#onApplyWindowInsets} will be called with the end state of the
         *     animation</li>
         *     <li>View hierarchy gets laid out according to the changes the application has
         *     requested due to the new insets being dispatched</li>
         *     <li>{@link #onStart} is called <em>before</em> the view
         *     hierarchy gets drawn in the new laid out state</li>
         *     <li>{@link #onProgress} is called immediately after with the animation start
         *     state</li>
         *     <li>The frame gets drawn.</li>
         * </ul>
         * <p>
         * This ordering allows the application to inspect the end state after the animation has
         * finished, and then revert to the starting state of the animation in the first
         * {@link #onProgress} callback by using post-layout view properties like {@link View#setX}
         * and related methods.
         *
         * <p>Note that the animation might be cancelled before {@link #onStart} is dispatched. On
         * {@link android.os.Build.VERSION_CODES#S S} and later, {@link #onEnd} is immediately
         * dispatched without an {@link #onStart} in that case.
         * On {@link android.os.Build.VERSION_CODES#R R}, no callbacks are dispatched after
         * {@code #onPrepare} for such an animation.
         *
         * <p>
         * Note: If the animation is application controlled by using
         * {@link WindowInsetsController#controlWindowInsetsAnimation}, the end state of the
         * animation is undefined as the application may decide on the end state only by passing in
         * {@code shown} parameter when calling {@link WindowInsetsAnimationController#finish}. In
         * this situation, the system will dispatch the insets in the opposite visibility state
         * before the animation starts. Example: When controlling the input method with
         * {@link WindowInsetsController#controlWindowInsetsAnimation} and the input method is
         * currently showing, {@link View#onApplyWindowInsets} will receive a {@link WindowInsets}
         * instance for which {@link WindowInsets#isVisible} will return {@code false} for
         * {@link WindowInsets.Type#ime}.
         *
         * @param animation The animation that is about to start.
         */
        public void onPrepare(@NonNull WindowInsetsAnimation animation) {
        }

        /**
         * Called when an insets animation gets started.
         * <p>
         * Note that, like {@link #onProgress}, dispatch of the animation start event is
         * hierarchical: It will starts at the root of the view hierarchy and then traverse it
         * and invoke the callback of the specific {@link View} that is being traversed.
         * The method may return a modified
         * instance of the bounds by calling {@link Bounds#inset} to indicate that a part of
         * the insets have been used to offset or clip its children, and the children shouldn't
         * worry about that part anymore. Furthermore, if {@link #getDispatchMode()} returns
         * {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore.
         *
         * @param animation The animation that is about to start.
         * @param bounds The bounds in which animation happens.
         * @return The animation representing the part of the insets that should be dispatched to
         *         the subtree of the hierarchy.
         */
        @NonNull
        public Bounds onStart(
                @NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds) {
            return bounds;
        }

        /**
         * Called when the insets change as part of running an animation. Note that even if multiple
         * animations for different types are running, there will only be one progress callback per
         * frame. The {@code insets} passed as an argument represents the overall state and will
         * include all types, regardless of whether they are animating or not.
         * <p>
         * Note that insets dispatch is hierarchical: It will start at the root of the view
         * hierarchy, and then traverse it and invoke the callback of the specific {@link View}
         * being traversed. The method may return a modified instance by calling
         * {@link WindowInsets#inset(int, int, int, int)} to indicate that a part of the insets have
         * been used to offset or clip its children, and the children shouldn't worry about that
         * part anymore. Furthermore, if {@link #getDispatchMode()} returns
         * {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore.
         *
         * @param insets The current insets.
         * @param runningAnimations The currently running animations.
         * @return The insets to dispatch to the subtree of the hierarchy.
         */
        @NonNull
        public abstract WindowInsets onProgress(@NonNull WindowInsets insets,
                @NonNull List<WindowInsetsAnimation> runningAnimations);

        /**
         * Called when an insets animation has ended.
         *
         * @param animation The animation that has ended. This will be the same instance
         *                  as passed into {@link #onStart}
         */
        public void onEnd(@NonNull WindowInsetsAnimation animation) {
        }

    }
}
