/*
 * 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.systemui.statusbar.notification.stack;

import static com.android.systemui.Flags.physicalNotificationMovement;
import static com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.TAG_ANIMATOR_TRANSLATION_Y;
import static com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Y_TRANSLATION;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.util.Log;
import android.util.Property;
import android.view.View;
import android.view.animation.Interpolator;

import com.android.app.animation.Interpolators;
import com.android.internal.dynamicanimation.animation.DynamicAnimation;
import com.android.internal.dynamicanimation.animation.SpringAnimation;
import com.android.systemui.Dumpable;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.NotificationFadeAware.FadeOptimizedNotification;
import com.android.systemui.statusbar.notification.PhysicsProperty;
import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator;
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.PropertyData;
import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
import com.android.systemui.statusbar.notification.row.ExpandableView;

import kotlin.Unit;
import kotlin.jvm.functions.Function1;

import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * A state of a view. This can be used to apply a set of view properties to a view with
 * {@link com.android.systemui.statusbar.notification.stack.StackScrollState} or start
 * animations with {@link com.android.systemui.statusbar.notification.stack.StackStateAnimator}.
 */
public class ViewState implements Dumpable {

    public ViewState() {
        this(physicalNotificationMovement());
    }

    public ViewState(boolean usePhysicsForMovement) {
        setUsePhysicsForMovement(usePhysicsForMovement);
    }

    /**
     * Some animation properties that can be used to update running animations but not creating
     * any new ones.
     */
    protected static final AnimationProperties NO_NEW_ANIMATIONS = new AnimationProperties() {
        AnimationFilter mAnimationFilter = new AnimationFilter();

        @Override
        public AnimationFilter getAnimationFilter() {
            return mAnimationFilter;
        }
    };
    private static final int TAG_ANIMATOR_TRANSLATION_X = R.id.translation_x_animator_tag;
    private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
    private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
    private static final int TAG_END_TRANSLATION_X = R.id.translation_x_animator_end_value_tag;
    private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
    private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
    private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
    private static final int TAG_START_TRANSLATION_X = R.id.translation_x_animator_start_value_tag;
    private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
    private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
    private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
    private static final String LOG_TAG = "StackViewState";

    private static final AnimatableProperty SCALE_X_PROPERTY = new AnimatableProperty() {

        @Override
        public int getAnimationStartTag() {
            return R.id.scale_x_animator_start_value_tag;
        }

        @Override
        public int getAnimationEndTag() {
            return R.id.scale_x_animator_end_value_tag;
        }

        @Override
        public int getAnimatorTag() {
            return R.id.scale_x_animator_tag;
        }

        @Override
        public Property getProperty() {
            return View.SCALE_X;
        }
    };

    private static final AnimatableProperty SCALE_Y_PROPERTY = new AnimatableProperty() {

        @Override
        public int getAnimationStartTag() {
            return R.id.scale_y_animator_start_value_tag;
        }

        @Override
        public int getAnimationEndTag() {
            return R.id.scale_y_animator_end_value_tag;
        }

        @Override
        public int getAnimatorTag() {
            return R.id.scale_y_animator_tag;
        }

        @Override
        public Property getProperty() {
            return View.SCALE_Y;
        }
    };

    public boolean gone;
    public boolean hidden;

    private float mAlpha;
    private float mXTranslation;
    private float mYTranslation;
    private float mZTranslation;
    private float mScaleX = 1.0f;
    private float mScaleY = 1.0f;
    protected boolean mUsePhysicsForMovement = false;

    public float getAlpha() {
        return mAlpha;
    }

    public void setUsePhysicsForMovement(boolean usePhysicsForMovement) {
        this.mUsePhysicsForMovement = usePhysicsForMovement;
    }

    /**
     * @param alpha View transparency.
     */
    public void setAlpha(float alpha) {
        if (isValidFloat(alpha, "alpha")) {
            this.mAlpha = alpha;
        }
    }

    public float getXTranslation() {
        return mXTranslation;
    }

    /**
     * @param xTranslation x-axis translation value for the animation.
     */
    public void setXTranslation(float xTranslation) {
        if (isValidFloat(xTranslation, "xTranslation")) {
            this.mXTranslation = xTranslation;
        }
    }

    public float getYTranslation() {
        return mYTranslation;
    }

    /**
     * @param yTranslation y-axis translation value for the animation.
     */
    public void setYTranslation(float yTranslation) {
        if (isValidFloat(yTranslation, "yTranslation")) {
            this.mYTranslation = yTranslation;
        }
    }

    public float getZTranslation() {
        return mZTranslation;
    }


    /**
     * @param zTranslation z-axis translation value for the animation.
     */
    public void setZTranslation(float zTranslation) {
        if (isValidFloat(zTranslation, "zTranslation")) {
            this.mZTranslation = zTranslation;
        }
    }

    public float getScaleX() {
        return mScaleX;
    }

    /**
     * @param scaleX x-axis scale property for the animation.
     */
    public void setScaleX(float scaleX) {
        if (isValidFloat(scaleX, "scaleX")) {
            this.mScaleX = scaleX;
        }
    }

    public float getScaleY() {
        return mScaleY;
    }

    /**
     * @param scaleY y-axis scale property for the animation.
     */
    public void setScaleY(float scaleY) {
        if (isValidFloat(scaleY, "scaleY")) {
            this.mScaleY = scaleY;
        }
    }

    /**
     * Checks if {@code value} is a valid float value. If it is not, logs it (using {@code name})
     * and returns false.
     */
    private boolean isValidFloat(float value, String name) {
        if (Float.isNaN(value)) {
            Log.wtf(LOG_TAG, "Cannot set property " + name + " to NaN");
            return false;
        }
        return true;
    }

    public void copyFrom(ViewState viewState) {
        mAlpha = viewState.mAlpha;
        mXTranslation = viewState.mXTranslation;
        mYTranslation = viewState.mYTranslation;
        mZTranslation = viewState.mZTranslation;
        gone = viewState.gone;
        hidden = viewState.hidden;
        mScaleX = viewState.mScaleX;
        mScaleY = viewState.mScaleY;
        mUsePhysicsForMovement = viewState.mUsePhysicsForMovement;
    }

    public void initFrom(View view) {
        mAlpha = view.getAlpha();
        mXTranslation = view.getTranslationX();
        mYTranslation = view.getTranslationY();
        mZTranslation = view.getTranslationZ();
        gone = view.getVisibility() == View.GONE;
        hidden = view.getVisibility() == View.INVISIBLE;
        mScaleX = view.getScaleX();
        mScaleY = view.getScaleY();
    }

    /**
     * Applies a {@link ViewState} to a normal view.
     */
    public void applyToView(View view) {
        if (this.gone) {
            // don't do anything with it
            return;
        }

        // apply xTranslation
        boolean animatingX = isAnimating(view, TAG_ANIMATOR_TRANSLATION_X);
        if (animatingX) {
            updateAnimationX(view);
        } else if (view.getTranslationX() != this.mXTranslation) {
            view.setTranslationX(this.mXTranslation);
        }

        // apply yTranslation
        if (mUsePhysicsForMovement) {
            PhysicsPropertyAnimator.setProperty(view, Y_TRANSLATION, this.mYTranslation);
        } else {
            boolean animatingY = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y);
            if (animatingY) {
                updateAnimationY(view);
            } else if (view.getTranslationY() != this.mYTranslation) {
                view.setTranslationY(this.mYTranslation);
            }
        }

        // apply zTranslation
        boolean animatingZ = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z);
        if (animatingZ) {
            updateAnimationZ(view);
        } else if (view.getTranslationZ() != this.mZTranslation) {
            view.setTranslationZ(this.mZTranslation);
        }

        // apply scaleX
        boolean animatingScaleX = isAnimating(view, SCALE_X_PROPERTY);
        if (animatingScaleX) {
            updateAnimation(view, SCALE_X_PROPERTY, mScaleX);
        } else if (view.getScaleX() != mScaleX) {
            view.setScaleX(mScaleX);
        }

        // apply scaleY
        boolean animatingScaleY = isAnimating(view, SCALE_Y_PROPERTY);
        if (animatingScaleY) {
            updateAnimation(view, SCALE_Y_PROPERTY, mScaleY);
        } else if (view.getScaleY() != mScaleY) {
            view.setScaleY(mScaleY);
        }

        int oldVisibility = view.getVisibility();
        boolean becomesInvisible = this.mAlpha == 0.0f || (this.hidden && (!isAnimating(view)
                || oldVisibility != View.VISIBLE));
        boolean animatingAlpha = isAnimating(view, TAG_ANIMATOR_ALPHA);
        if (animatingAlpha) {
            updateAlphaAnimation(view);
        } else if (view.getAlpha() != this.mAlpha) {
            // apply layer type
            boolean becomesFullyVisible = this.mAlpha == 1.0f;
            boolean becomesFaded = !becomesInvisible && !becomesFullyVisible;
            if (FadeOptimizedNotification.FADE_LAYER_OPTIMIZATION_ENABLED
                    && view instanceof FadeOptimizedNotification) {
                // NOTE: A view that's going to utilize this interface to avoid having a hardware
                //  layer will have to return false from hasOverlappingRendering(), so we
                //  intentionally do not check that value in this if, even though we do in the else.
                FadeOptimizedNotification fadeOptimizedView = (FadeOptimizedNotification) view;
                boolean isFaded = fadeOptimizedView.isNotificationFaded();
                if (isFaded != becomesFaded) {
                    fadeOptimizedView.setNotificationFaded(becomesFaded);
                }
            } else {
                boolean newLayerTypeIsHardware = becomesFaded && view.hasOverlappingRendering();
                int layerType = view.getLayerType();
                int newLayerType =
                        newLayerTypeIsHardware ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE;
                if (layerType != newLayerType) {
                    view.setLayerType(newLayerType, null);
                }
            }

            // apply alpha
            view.setAlpha(this.mAlpha);
        }

        // apply visibility
        int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE;
        if (newVisibility != oldVisibility) {
            if (!(view instanceof ExpandableView) || !((ExpandableView) view).willBeGone()) {
                // We don't want views to change visibility when they are animating to GONE
                view.setVisibility(newVisibility);
            }
        }
    }

    public boolean isAnimating(View view) {
        if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_X)) {
            return true;
        }
        if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y)) {
            return true;
        }
        if (isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z)) {
            return true;
        }
        if (isAnimating(view, TAG_ANIMATOR_ALPHA)) {
            return true;
        }
        if (isAnimating(view, SCALE_X_PROPERTY)) {
            return true;
        }
        if (isAnimating(view, SCALE_Y_PROPERTY)) {
            return true;
        }
        return false;
    }

    private static boolean isAnimating(View view, int tag) {
        Object childTag = getChildTag(view, tag);
        if (childTag instanceof PropertyData propertyData) {
            return propertyData.getAnimator() != null;
        }
        return childTag != null;
    }

    public static boolean isAnimating(View view, AnimatableProperty property) {
        Object childTag = getChildTag(view, property.getAnimatorTag());
        if (childTag instanceof PropertyData propertyData) {
            return propertyData.getAnimator() != null;
        }
        return childTag != null;
    }

    public static boolean isAnimating(View view, PhysicsProperty property) {
        Object childTag = getChildTag(view, property.getTag());
        if (childTag instanceof PropertyData propertyData) {
            return propertyData.getAnimator() != null;
        }
        return childTag != null;
    }

    /**
     * Start an animation to this viewstate
     *
     * @param child               the view to animate
     * @param animationProperties the properties of the animation
     */
    public void animateTo(View child, AnimationProperties animationProperties) {
        boolean wasVisible = child.getVisibility() == View.VISIBLE;
        final float alpha = this.mAlpha;
        if (!wasVisible && (alpha != 0 || child.getAlpha() != 0) && !this.gone && !this.hidden) {
            child.setVisibility(View.VISIBLE);
        }
        float childAlpha = child.getAlpha();
        boolean alphaChanging = this.mAlpha != childAlpha;
        if (child instanceof ExpandableView) {
            // We don't want views to change visibility when they are animating to GONE
            alphaChanging &= !((ExpandableView) child).willBeGone();
        }

        // start translationX animation
        if (child.getTranslationX() != this.mXTranslation) {
            startXTranslationAnimation(child, animationProperties);
        } else {
            abortAnimation(child, TAG_ANIMATOR_TRANSLATION_X);
        }

        // start translationY animation
        if (child.getTranslationY() != this.mYTranslation) {
            startYTranslationAnimation(child, animationProperties);
        } else {
            abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Y);
        }

        // start translationZ animation
        if (child.getTranslationZ() != this.mZTranslation) {
            startZTranslationAnimation(child, animationProperties);
        } else {
            abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z);
        }

        // start scaleX animation
        if (child.getScaleX() != mScaleX) {
            PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, mScaleX, animationProperties);
        } else {
            abortAnimation(child, SCALE_X_PROPERTY.getAnimatorTag());
        }

        // start scaleX animation
        if (child.getScaleY() != mScaleY) {
            PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, mScaleY, animationProperties);
        } else {
            abortAnimation(child, SCALE_Y_PROPERTY.getAnimatorTag());
        }

        // start alpha animation
        if (alphaChanging) {
            startAlphaAnimation(child, animationProperties);
        } else {
            abortAnimation(child, TAG_ANIMATOR_ALPHA);
        }
    }

    private void updateAlphaAnimation(View view) {
        startAlphaAnimation(view, NO_NEW_ANIMATIONS);
    }

    private void startAlphaAnimation(final View child, AnimationProperties properties) {
        Float previousStartValue = getChildTag(child, TAG_START_ALPHA);
        Float previousEndValue = getChildTag(child, TAG_END_ALPHA);
        final float newEndValue = this.mAlpha;
        if (previousEndValue != null && previousEndValue == newEndValue) {
            return;
        }
        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
        AnimationFilter filter = properties.getAnimationFilter();
        if (!filter.animateAlpha) {
            // just a local update was performed
            if (previousAnimator != null) {
                // we need to increase all animation keyframes of the previous animator by the
                // relative change to the end value
                PropertyValuesHolder[] values = previousAnimator.getValues();
                float relativeDiff = newEndValue - previousEndValue;
                float newStartValue = previousStartValue + relativeDiff;
                values[0].setFloatValues(newStartValue, newEndValue);
                child.setTag(TAG_START_ALPHA, newStartValue);
                child.setTag(TAG_END_ALPHA, newEndValue);
                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                return;
            } else {
                // no new animation needed, let's just apply the value
                child.setAlpha(newEndValue);
                if (newEndValue == 0) {
                    child.setVisibility(View.INVISIBLE);
                }
            }
        }

        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA, child.getAlpha(),
                newEndValue);
        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
        // Handle layer type
        child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
        animator.addListener(new AnimatorListenerAdapter() {
            public boolean mWasCancelled;

            @Override
            public void onAnimationEnd(Animator animation) {
                child.setLayerType(View.LAYER_TYPE_NONE, null);
                if (newEndValue == 0 && !mWasCancelled) {
                    child.setVisibility(View.INVISIBLE);
                }
                // remove the tag when the animation is finished
                child.setTag(TAG_ANIMATOR_ALPHA, null);
                child.setTag(TAG_START_ALPHA, null);
                child.setTag(TAG_END_ALPHA, null);
            }

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

            @Override
            public void onAnimationStart(Animator animation) {
                mWasCancelled = false;
            }
        });
        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
        animator.setDuration(newDuration);
        if (properties.delay > 0 && (previousAnimator == null
                || previousAnimator.getAnimatedFraction() == 0)) {
            animator.setStartDelay(properties.delay);
        }
        AnimatorListenerAdapter listener = properties.getAnimationFinishListener(View.ALPHA);
        if (listener != null) {
            animator.addListener(listener);
        }

        startAnimator(animator, listener);
        child.setTag(TAG_ANIMATOR_ALPHA, animator);
        child.setTag(TAG_START_ALPHA, child.getAlpha());
        child.setTag(TAG_END_ALPHA, newEndValue);
    }

    private void updateAnimationZ(View view) {
        startZTranslationAnimation(view, NO_NEW_ANIMATIONS);
    }

    private void updateAnimation(View view, AnimatableProperty property, float endValue) {
        PropertyAnimator.startAnimation(view, property, endValue, NO_NEW_ANIMATIONS);
    }

    private void startZTranslationAnimation(final View child, AnimationProperties properties) {
        Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_Z);
        Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_Z);
        float newEndValue = this.mZTranslation;
        if (previousEndValue != null && previousEndValue == newEndValue) {
            return;
        }
        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
        AnimationFilter filter = properties.getAnimationFilter();
        if (!filter.animateZ) {
            // just a local update was performed
            if (previousAnimator != null) {
                // we need to increase all animation keyframes of the previous animator by the
                // relative change to the end value
                PropertyValuesHolder[] values = previousAnimator.getValues();
                float relativeDiff = newEndValue - previousEndValue;
                float newStartValue = previousStartValue + relativeDiff;
                values[0].setFloatValues(newStartValue, newEndValue);
                child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
                child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                return;
            } else {
                // no new animation needed, let's just apply the value
                child.setTranslationZ(newEndValue);
            }
        }

        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
                child.getTranslationZ(), newEndValue);
        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
        animator.setDuration(newDuration);
        if (properties.delay > 0 && (previousAnimator == null
                || previousAnimator.getAnimatedFraction() == 0)) {
            animator.setStartDelay(properties.delay);
        }
        AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
                View.TRANSLATION_Z);
        if (listener != null) {
            animator.addListener(listener);
        }
        // remove the tag when the animation is finished
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
                child.setTag(TAG_START_TRANSLATION_Z, null);
                child.setTag(TAG_END_TRANSLATION_Z, null);
            }
        });
        startAnimator(animator, listener);
        child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
        child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
        child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
    }

    private void updateAnimationX(View view) {
        startXTranslationAnimation(view, NO_NEW_ANIMATIONS);
    }

    private void startXTranslationAnimation(final View child, AnimationProperties properties) {
        Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_X);
        Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_X);
        float newEndValue = this.mXTranslation;
        if (previousEndValue != null && previousEndValue == newEndValue) {
            return;
        }
        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_X);
        AnimationFilter filter = properties.getAnimationFilter();
        if (!filter.animateX) {
            // just a local update was performed
            if (previousAnimator != null) {
                // we need to increase all animation keyframes of the previous animator by the
                // relative change to the end value
                PropertyValuesHolder[] values = previousAnimator.getValues();
                float relativeDiff = newEndValue - previousEndValue;
                float newStartValue = previousStartValue + relativeDiff;
                values[0].setFloatValues(newStartValue, newEndValue);
                child.setTag(TAG_START_TRANSLATION_X, newStartValue);
                child.setTag(TAG_END_TRANSLATION_X, newEndValue);
                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                return;
            } else {
                // no new animation needed, let's just apply the value
                child.setTranslationX(newEndValue);
                return;
            }
        }

        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_X,
                child.getTranslationX(), newEndValue);
        Interpolator customInterpolator = properties.getCustomInterpolator(child,
                View.TRANSLATION_X);
        Interpolator interpolator =
                customInterpolator != null ? customInterpolator : Interpolators.FAST_OUT_SLOW_IN;
        animator.setInterpolator(interpolator);
        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
        animator.setDuration(newDuration);
        if (properties.delay > 0 && (previousAnimator == null
                || previousAnimator.getAnimatedFraction() == 0)) {
            animator.setStartDelay(properties.delay);
        }
        AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
                View.TRANSLATION_X);
        if (listener != null) {
            animator.addListener(listener);
        }
        // remove the tag when the animation is finished
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                child.setTag(TAG_ANIMATOR_TRANSLATION_X, null);
                child.setTag(TAG_START_TRANSLATION_X, null);
                child.setTag(TAG_END_TRANSLATION_X, null);
            }
        });
        startAnimator(animator, listener);
        child.setTag(TAG_ANIMATOR_TRANSLATION_X, animator);
        child.setTag(TAG_START_TRANSLATION_X, child.getTranslationX());
        child.setTag(TAG_END_TRANSLATION_X, newEndValue);
    }

    private void updateAnimationY(View view) {
        startYTranslationAnimation(view, NO_NEW_ANIMATIONS);
    }

    private void startYTranslationAnimation(final View child, AnimationProperties properties) {
        if (mUsePhysicsForMovement) {
            // Y Translation does some extra calls when it ends, so lets add a listener
            DynamicAnimation.OnAnimationEndListener endListener = null;
            if (!isAnimatingY(child)) {
                // Only add a listener if we're not animating yet
                endListener =
                        (animation, canceled, value, velocity) -> {
                            if (!canceled) {
                                HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child,
                                        false);
                                onYTranslationAnimationFinished(child);
                            }
                        };
            }
            PhysicsPropertyAnimator.setProperty(child, Y_TRANSLATION, this.mYTranslation,
                    properties, properties.getAnimationFilter().animateY, endListener);
        } else {
            startYTranslationInterpolatorAnimation(child, properties);
        }
    }

    private void startYTranslationInterpolatorAnimation(View child,
            AnimationProperties properties) {
        Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_Y);
        Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_Y);
        float newEndValue = this.mYTranslation;
        if (previousEndValue != null && previousEndValue == newEndValue) {
            return;
        }
        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
        AnimationFilter filter = properties.getAnimationFilter();
        if (!filter.animateY) {
            // just a local update was performed
            if (previousAnimator != null) {
                // we need to increase all animation keyframes of the previous animator by the
                // relative change to the end value
                PropertyValuesHolder[] values = previousAnimator.getValues();
                float relativeDiff = newEndValue - previousEndValue;
                float newStartValue = previousStartValue + relativeDiff;
                values[0].setFloatValues(newStartValue, newEndValue);
                child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
                child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                return;
            } else {
                // no new animation needed, let's just apply the value
                child.setTranslationY(newEndValue);
                return;
            }
        }

        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
                child.getTranslationY(), newEndValue);
        Interpolator customInterpolator = properties.getCustomInterpolator(child,
                View.TRANSLATION_Y);
        Interpolator interpolator =
                customInterpolator != null ? customInterpolator : Interpolators.FAST_OUT_SLOW_IN;
        animator.setInterpolator(interpolator);
        long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
        animator.setDuration(newDuration);
        if (properties.delay > 0 && (previousAnimator == null
                || previousAnimator.getAnimatedFraction() == 0)) {
            animator.setStartDelay(properties.delay);
        }
        AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
                View.TRANSLATION_Y);
        if (listener != null) {
            animator.addListener(listener);
        }
        // remove the tag when the animation is finished
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child, false);
                child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
                child.setTag(TAG_START_TRANSLATION_Y, null);
                child.setTag(TAG_END_TRANSLATION_Y, null);
                onYTranslationAnimationFinished(child);
            }
        });
        startAnimator(animator, listener);
        child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
        child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
        child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
    }

    protected void onYTranslationAnimationFinished(View view) {
        if (hidden && !gone) {
            view.setVisibility(View.INVISIBLE);
        }
    }

    public static void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
        if (listener != null) {
            // Even if there's a delay we'd want to notify it of the start immediately.
            listener.onAnimationStart(animator);
        }
        animator.start();
    }

    public static <T> T getChildTag(View child, int tag) {
        return (T) child.getTag(tag);
    }

    protected void abortAnimation(View child, int animatorTag) {
        Object storedTag = getChildTag(child, animatorTag);
        if (storedTag != null) {
            if (storedTag instanceof Animator animator) {
                animator.cancel();
            } else if (storedTag instanceof PropertyData propertyData) {
                // Physics based animation!
                Runnable delayRunnable = propertyData.getDelayRunnable();
                child.removeCallbacks(delayRunnable);
                SpringAnimation animator = propertyData.getAnimator();
                if (animator != null) {
                    boolean wasRunning = animator.isRunning();
                    animator.cancel();
                    if (!wasRunning) {
                        // The animation was never started, so the cancel above doesn't do much.
                        // We need to notify the endListeners manually that the animation has ended
                        // since they need to reset some state.
                        Function1<Boolean, Unit> listener =
                                propertyData.getEndedBeforeStartingCleanupHandler();
                        if (listener != null) {
                            listener.invoke(true /* cancelled */);
                        }
                    }
                }
            }
        }
    }

    protected void skipAnimationToEnd(View child, int animatorTag) {
        Object storedTag = getChildTag(child, animatorTag);
        if (storedTag != null) {
            if (storedTag instanceof Animator animator) {
                animator.end();
            } else if (storedTag instanceof PropertyData propertyData) {
                // Physics based animation!
                Runnable delayRunnable = propertyData.getDelayRunnable();
                child.removeCallbacks(delayRunnable);
                SpringAnimation animator = propertyData.getAnimator();
                if (animator != null) {
                    boolean wasRunning = animator.isRunning();
                    animator.skipToEnd();
                    if (!wasRunning) {
                        // The animation was ever started, so the skipToEnd above doesn't do much.
                        // We need to notify the endListeners manually that the animation has ended
                        // since they need to reset some state.
                        Function1<Boolean, Unit> listener =
                                propertyData.getEndedBeforeStartingCleanupHandler();
                        if (listener != null) {
                            listener.invoke(false /* cancelled */);
                        }
                    }
                }
            }
        }
    }

    /**
     * Cancel the previous animator and get the duration of the new animation.
     *
     * @param duration         the new duration
     * @param previousAnimator the animator which was running before
     * @return the new duration
     */
    public static long cancelAnimatorAndGetNewDuration(long duration,
            ValueAnimator previousAnimator) {
        long newDuration = duration;
        if (previousAnimator != null) {
            // We take either the desired length of the new animation or the remaining time of
            // the previous animator, whichever is longer.
            newDuration = Math.max(
                    previousAnimator.getDuration() - previousAnimator.getCurrentPlayTime(),
                    newDuration);
            previousAnimator.cancel();
        }
        return newDuration;
    }

    /**
     * Get the end value of the zTranslation animation running on a view or the zTranslation
     * if no animation is running.
     */
    public static float getFinalTranslationZ(View view) {
        if (view == null) {
            return 0;
        }
        ValueAnimator zAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Z);
        if (zAnimator == null) {
            return view.getTranslationZ();
        } else {
            return getChildTag(view, TAG_END_TRANSLATION_Z);
        }
    }

    public static boolean isAnimatingY(View child) {
        return isAnimating(child, TAG_ANIMATOR_TRANSLATION_Y);
    }

    /**
     * Abort the animations. Unlike {@link #finishAnimations(View)} this causes the animators
     * to stop in their tracks.
     */
    public void cancelAnimations(View view) {
        abortAnimation(view, TAG_ANIMATOR_TRANSLATION_X);
        abortAnimation(view, TAG_ANIMATOR_TRANSLATION_Y);
        abortAnimation(view, TAG_ANIMATOR_TRANSLATION_Z);
        abortAnimation(view, TAG_ANIMATOR_ALPHA);
    }

    /**
     * Finish the animations. This causes any currently running animators to jump to the end
     * value of the property being animated, and to trigger any animation end listeners.
     */
    public void finishAnimations(View view) {
        skipAnimationToEnd(view, TAG_ANIMATOR_TRANSLATION_X);
        skipAnimationToEnd(view, TAG_ANIMATOR_TRANSLATION_Y);
        skipAnimationToEnd(view, TAG_ANIMATOR_TRANSLATION_Z);
        skipAnimationToEnd(view, TAG_ANIMATOR_ALPHA);
    }

    @Override
    public void dump(PrintWriter pw, String[] args) {
        StringBuilder result = new StringBuilder();
        result.append("ViewState { ");

        boolean first = true;
        Class currentClass = this.getClass();
        while (currentClass != null) {
            Field[] fields = currentClass.getDeclaredFields();
            // Print field names paired with their values
            for (Field field : fields) {
                int modifiers = field.getModifiers();
                if (Modifier.isStatic(modifiers) || field.isSynthetic() || Modifier.isTransient(
                        modifiers)) {
                    continue;
                }
                if (!first) {
                    result.append(", ");
                }
                try {
                    result.append(field.getName());
                    result.append(": ");
                    //requires access to private field:
                    field.setAccessible(true);
                    result.append(field.get(this));
                } catch (IllegalAccessException ex) {
                }
                first = false;
            }
            currentClass = currentClass.getSuperclass();
        }
        result.append(" }");
        pw.print(result);
    }
}
