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

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.os.Handler;
import android.util.Log;
import android.util.MathUtils;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.CornerHandleView;
import com.android.systemui.R;
import com.android.systemui.statusbar.phone.NavigationBarTransitions;

/**
 * A class for managing Assistant handle show, hide and animation.
 */
public class AssistHandleViewController implements NavigationBarTransitions.DarkIntensityListener {

    private static final boolean DEBUG = false;
    private static final String TAG = "AssistHandleViewController";

    private Handler mHandler;
    private CornerHandleView mAssistHintLeft;
    private CornerHandleView mAssistHintRight;
    private int mBottomOffset;

    @VisibleForTesting
    boolean mAssistHintVisible;
    @VisibleForTesting
    boolean mAssistHintBlocked = false;

    public AssistHandleViewController(Handler handler, View navBar) {
        mHandler = handler;
        mAssistHintLeft = navBar.findViewById(R.id.assist_hint_left);
        mAssistHintRight = navBar.findViewById(R.id.assist_hint_right);
    }

    @Override
    public void onDarkIntensity(float darkIntensity) {
        mAssistHintLeft.updateDarkness(darkIntensity);
        mAssistHintRight.updateDarkness(darkIntensity);
    }

    /**
     * Set the bottom offset.
     *
     * @param bottomOffset the bottom offset to translate.
     */
    public void setBottomOffset(int bottomOffset) {
        if (mBottomOffset != bottomOffset) {
            mBottomOffset = bottomOffset;
            if (mAssistHintVisible) {
                // If assist handles are visible, hide them without animation and then make them
                // show once again (with corrected bottom offset).
                hideAssistHandles();
                setAssistHintVisible(true);
            }
        }
    }

    /**
     * Controls the visibility of the assist gesture handles.
     *
     * @param visible whether the handles should be shown
     */
    public void setAssistHintVisible(boolean visible) {
        if (!mHandler.getLooper().isCurrentThread()) {
            mHandler.post(() -> setAssistHintVisible(visible));
            return;
        }

        if (mAssistHintBlocked && visible) {
            if (DEBUG) {
                Log.v(TAG, "Assist hint blocked, cannot make it visible");
            }
            return;
        }

        if (mAssistHintVisible != visible) {
            mAssistHintVisible = visible;
            fade(mAssistHintLeft, mAssistHintVisible, /* isLeft = */ true);
            fade(mAssistHintRight, mAssistHintVisible, /* isLeft = */ false);
        }
    }

    /**
     * Prevents the assist hint from becoming visible even if `mAssistHintVisible` is true.
     */
    public void setAssistHintBlocked(boolean blocked) {
        if (!mHandler.getLooper().isCurrentThread()) {
            mHandler.post(() -> setAssistHintBlocked(blocked));
            return;
        }

        mAssistHintBlocked = blocked;
        if (mAssistHintVisible && mAssistHintBlocked) {
            hideAssistHandles();
        }
    }

    private void hideAssistHandles() {
        mAssistHintLeft.setVisibility(View.GONE);
        mAssistHintRight.setVisibility(View.GONE);
        mAssistHintVisible = false;
    }

    /**
     * Returns an animator that animates the given view from start to end over durationMs. Start and
     * end represent total animation progress: 0 is the start, 1 is the end, 1.1 would be an
     * overshoot.
     */
    Animator getHandleAnimator(View view, float start, float end, boolean isLeft, long durationMs,
            Interpolator interpolator) {
        // Note that lerp does allow overshoot, in cases where start and end are outside of [0,1].
        float scaleStart = MathUtils.lerp(2f, 1f, start);
        float scaleEnd = MathUtils.lerp(2f, 1f, end);
        Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, scaleStart, scaleEnd);
        Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, scaleStart, scaleEnd);
        float translationStart = MathUtils.lerp(0.2f, 0f, start);
        float translationEnd = MathUtils.lerp(0.2f, 0f, end);
        int xDirection = isLeft ? -1 : 1;
        Animator translateX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
                xDirection * translationStart * view.getWidth(),
                xDirection * translationEnd * view.getWidth());
        Animator translateY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y,
                translationStart * view.getHeight() + mBottomOffset,
                translationEnd * view.getHeight() + mBottomOffset);

        AnimatorSet set = new AnimatorSet();
        set.play(scaleX).with(scaleY);
        set.play(scaleX).with(translateX);
        set.play(scaleX).with(translateY);
        set.setDuration(durationMs);
        set.setInterpolator(interpolator);
        return set;
    }

    private void fade(View view, boolean fadeIn, boolean isLeft) {
        if (fadeIn) {
            view.animate().cancel();
            view.setAlpha(1f);
            view.setVisibility(View.VISIBLE);

            // A piecewise spring-like interpolation.
            // End value in one animator call must match the start value in the next, otherwise
            // there will be a discontinuity.
            AnimatorSet anim = new AnimatorSet();
            Animator first = getHandleAnimator(view, 0, 1.1f, isLeft, 750,
                    new PathInterpolator(0, 0.45f, .67f, 1f));
            Interpolator secondInterpolator = new PathInterpolator(0.33f, 0, 0.67f, 1f);
            Animator second = getHandleAnimator(view, 1.1f, 0.97f, isLeft, 400,
                    secondInterpolator);
            Animator third = getHandleAnimator(view, 0.97f, 1.02f, isLeft, 400,
                    secondInterpolator);
            Animator fourth = getHandleAnimator(view, 1.02f, 1f, isLeft, 400,
                    secondInterpolator);
            anim.play(first).before(second);
            anim.play(second).before(third);
            anim.play(third).before(fourth);
            anim.start();
        } else {
            view.animate().cancel();
            view.animate()
                .setInterpolator(new AccelerateInterpolator(1.5f))
                .setDuration(250)
                .alpha(0f);
        }

    }
}
