/*
 * 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 com.android.systemui.accessibility;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiContext;
import android.content.Context;
import android.content.res.Resources;
import android.os.RemoteException;
import android.util.Log;
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import android.view.animation.AccelerateInterpolator;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.res.R;

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

/**
 * Provides same functionality of {@link WindowMagnificationController}. Some methods run with
 * the animation.
 */
class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUpdateListener,
        Animator.AnimatorListener {

    private static final String TAG = "WindowMagnificationAnimationController";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_DISABLING, STATE_ENABLING})
    @interface MagnificationState {}

    // The window magnification is disabled.
    @VisibleForTesting static final int STATE_DISABLED = 0;
    // The window magnification is enabled.
    @VisibleForTesting static final int STATE_ENABLED = 1;
    // The window magnification is going to be disabled when the animation is end.
    private static final int STATE_DISABLING = 2;
    // The animation is running for enabling the window magnification.
    private static final int STATE_ENABLING = 3;

    private WindowMagnificationController mController;
    private final ValueAnimator mValueAnimator;
    private final AnimationSpec mStartSpec = new AnimationSpec();
    private final AnimationSpec mEndSpec = new AnimationSpec();
    private float mMagnificationFrameOffsetRatioX = 0f;
    private float mMagnificationFrameOffsetRatioY = 0f;
    private final Context mContext;
    // Called when the animation is ended successfully without cancelling or mStartSpec and
    // mEndSpec are equal.
    private IRemoteMagnificationAnimationCallback mAnimationCallback;
    // The flag to ignore the animation end callback.
    private boolean mEndAnimationCanceled = false;
    @MagnificationState
    private int mState = STATE_DISABLED;

    WindowMagnificationAnimationController(@UiContext Context context) {
        this(context, newValueAnimator(context.getResources()));
    }

    @VisibleForTesting
    WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator) {
        mContext = context;
        mValueAnimator = valueAnimator;
        mValueAnimator.addUpdateListener(this);
        mValueAnimator.addListener(this);
    }

    void setWindowMagnificationController(@NonNull WindowMagnificationController controller) {
        mController = controller;
    }

    /**
     * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
     * float, float, IRemoteMagnificationAnimationCallback)}
     * with transition animation. If the window magnification is not enabled, the scale will start
     * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
     * {@code STATE_DISABLING}, the animation runs in reverse.
     *
     * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
     * @param centerX The screen-relative X coordinate around which to center,
     *                or {@link Float#NaN} to leave unchanged.
     * @param centerY The screen-relative Y coordinate around which to center,
     *                or {@link Float#NaN} to leave unchanged.
     * @param animationCallback Called when the transition is complete, the given arguments
     *                          are as same as current values, or the transition is interrupted
     *                          due to the new transition request.
     *
     * @see #onAnimationUpdate(ValueAnimator)
     */
    void enableWindowMagnification(float scale, float centerX, float centerY,
            @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
        enableWindowMagnification(scale, centerX, centerY, 0f, 0f, animationCallback);
    }

    /**
     * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
     * float, float, IRemoteMagnificationAnimationCallback)}
     * with transition animation. If the window magnification is not enabled, the scale will start
     * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
     * {@code STATE_DISABLING}, the animation runs in reverse.
     *
     * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
     * @param centerX The screen-relative X coordinate around which to center for magnification,
     *                or {@link Float#NaN} to leave unchanged.
     * @param centerY The screen-relative Y coordinate around which to center for magnification,
     *                or {@link Float#NaN} to leave unchanged.
     * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset between
     *                                       frame position X and centerX
     * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset between
     *                                       frame position Y and centerY
     * @param animationCallback Called when the transition is complete, the given arguments
     *                          are as same as current values, or the transition is interrupted
     *                          due to the new transition request.
     *
     * @see #onAnimationUpdate(ValueAnimator)
     */
    void enableWindowMagnification(float scale, float centerX, float centerY,
            float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
            @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
        if (mController == null) {
            return;
        }
        sendAnimationCallback(false);
        mMagnificationFrameOffsetRatioX = magnificationFrameOffsetRatioX;
        mMagnificationFrameOffsetRatioY = magnificationFrameOffsetRatioY;

        // Enable window magnification without animation immediately.
        if (animationCallback == null) {
            if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
                mValueAnimator.cancel();
            }
            mController.enableWindowMagnificationInternal(scale, centerX, centerY,
                    mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
            updateState();
            return;
        }
        mAnimationCallback = animationCallback;
        setupEnableAnimationSpecs(scale, centerX, centerY);

        if (mEndSpec.equals(mStartSpec)) {
            if (mState == STATE_DISABLED) {
                mController.enableWindowMagnificationInternal(scale, centerX, centerY,
                        mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
            } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
                mValueAnimator.cancel();
            }
            sendAnimationCallback(true);
            updateState();
        } else {
            if (mState == STATE_DISABLING) {
                mValueAnimator.reverse();
            } else {
                if (mState == STATE_ENABLING) {
                    mValueAnimator.cancel();
                }
                mValueAnimator.start();
            }
            setState(STATE_ENABLING);
        }
    }

    void moveWindowMagnifierToPosition(float centerX, float centerY,
            IRemoteMagnificationAnimationCallback callback) {
        if (mState == STATE_ENABLED) {
            // We set the animation duration to shortAnimTime which would be reset at the end.
            mValueAnimator.setDuration(mContext.getResources()
                    .getInteger(com.android.internal.R.integer.config_shortAnimTime));
            enableWindowMagnification(Float.NaN, centerX, centerY,
                    /* magnificationFrameOffsetRatioX */ Float.NaN,
                    /* magnificationFrameOffsetRatioY */ Float.NaN, callback);
        } else if (mState == STATE_ENABLING) {
            sendAnimationCallback(false);
            mAnimationCallback = callback;
            mValueAnimator.setDuration(mContext.getResources()
                    .getInteger(com.android.internal.R.integer.config_shortAnimTime));
            setupEnableAnimationSpecs(Float.NaN, centerX, centerY);
        }
    }

    private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) {
        if (mController == null) {
            return;
        }
        final float currentScale = mController.getScale();
        final float currentCenterX = mController.getCenterX();
        final float currentCenterY = mController.getCenterY();

        if (mState == STATE_DISABLED) {
            // We don't need to offset the center during the animation.
            mStartSpec.set(/* scale*/ 1.0f, centerX, centerY);
            mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger(
                    R.integer.magnification_default_scale) : scale, centerX, centerY);
        } else {
            mStartSpec.set(currentScale, currentCenterX, currentCenterY);

            final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale);
            final float endCenterX =
                    (mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX);
            final float endCenterY =
                    (mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY);

            mEndSpec.set(Float.isNaN(scale) ? endScale : scale,
                    Float.isNaN(centerX) ? endCenterX : centerX,
                    Float.isNaN(centerY) ? endCenterY : centerY);
        }
        if (DEBUG) {
            Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = "
                    + mEndSpec);
        }
    }

    /** Returns {@code true} if the animator is running. */
    boolean isAnimating() {
        return mValueAnimator.isRunning();
    }

    /**
     * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition
     * animation. If the window magnification is enabling, it runs the animation in reverse.
     *
     * @param animationCallback Called when the transition is complete, the given arguments
     *                          are as same as current values, or the transition is interrupted
     *                          due to the new transition request.
     */
    void deleteWindowMagnification(
            @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
        if (mController == null) {
            return;
        }
        sendAnimationCallback(false);
        // Delete window magnification without animation.
        if (animationCallback == null) {
            if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
                mValueAnimator.cancel();
            }
            mController.deleteWindowMagnification();
            updateState();
            return;
        }

        mAnimationCallback = animationCallback;
        if (mState == STATE_DISABLED || mState == STATE_DISABLING) {
            if (mState == STATE_DISABLED) {
                sendAnimationCallback(true);
            }
            return;
        }
        mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN);
        mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN);

        mValueAnimator.reverse();
        setState(STATE_DISABLING);
    }

    private void updateState() {
        if (Float.isNaN(mController.getScale())) {
            setState(STATE_DISABLED);
        } else {
            setState(STATE_ENABLED);
        }
    }

    private void setState(@MagnificationState int state) {
        if (DEBUG) {
            Log.d(TAG, "setState from " + mState + " to " + state);
        }
        mState = state;
    }

    @VisibleForTesting
    @MagnificationState int getState() {
        return mState;
    }

    @Override
    public void onAnimationStart(Animator animation) {
        mEndAnimationCanceled = false;
    }

    @Override
    public void onAnimationEnd(Animator animation, boolean isReverse) {
        if (mEndAnimationCanceled || mController == null) {
            return;
        }
        if (mState == STATE_DISABLING) {
            mController.deleteWindowMagnification();
        }
        updateState();
        sendAnimationCallback(true);
        // We reset the duration to config_longAnimTime
        mValueAnimator.setDuration(mContext.getResources()
                .getInteger(com.android.internal.R.integer.config_longAnimTime));
    }

    @Override
    public void onAnimationEnd(Animator animation) {
    }

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

    @Override
    public void onAnimationRepeat(Animator animation) {
    }

    private void sendAnimationCallback(boolean success) {
        if (mAnimationCallback != null) {
            try {
                mAnimationCallback.onResult(success);
                if (DEBUG) {
                    Log.d(TAG, "sendAnimationCallback success = " + success);
                }
            } catch (RemoteException e) {
                Log.w(TAG, "sendAnimationCallback failed : " + e);
            }
            mAnimationCallback = null;
        }
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        if (mController == null) {
            return;
        }
        final float fract = animation.getAnimatedFraction();
        final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract;
        final float centerX =
                mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract;
        final float centerY =
                mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract;
        mController.enableWindowMagnificationInternal(sentScale, centerX, centerY,
                mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
    }

    private static ValueAnimator newValueAnimator(Resources resource) {
        final ValueAnimator valueAnimator = new ValueAnimator();
        valueAnimator.setDuration(
                resource.getInteger(com.android.internal.R.integer.config_longAnimTime));
        valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f));
        valueAnimator.setFloatValues(0.0f, 1.0f);
        return valueAnimator;
    }

    private static class AnimationSpec {
        private float mScale = Float.NaN;
        private float mCenterX = Float.NaN;
        private float mCenterY = Float.NaN;

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }

            if (other == null || getClass() != other.getClass()) {
                return false;
            }

            final AnimationSpec s = (AnimationSpec) other;
            return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY;
        }

        @Override
        public int hashCode() {
            int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0);
            result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0);
            result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0);
            return result;
        }

        void set(float scale, float centerX, float centerY) {
            mScale = scale;
            mCenterX = centerX;
            mCenterY = centerY;
        }

        @Override
        public String toString() {
            return "AnimationSpec{"
                    + "mScale=" + mScale
                    + ", mCenterX=" + mCenterX
                    + ", mCenterY=" + mCenterY
                    + '}';
        }
    }
}
