/*
 * Copyright (C) 2017 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.server.wm;

import static android.internal.perfetto.protos.Animationadapter.AnimationSpecProto.WINDOW;
import static android.internal.perfetto.protos.Animationadapter.WindowAnimationSpecProto.ANIMATION;

import static com.android.server.wm.AnimationAdapter.STATUS_BAR_TRANSITION_DURATION;
import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_NONE;

import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.proto.ProtoOutputStream;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.view.animation.TranslateAnimation;

import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;

import java.io.PrintWriter;

/**
 * Animation spec for regular window animations.
 */
public class WindowAnimationSpec implements AnimationSpec {

    @NonNull
    private final Animation mAnimation;
    @NonNull
    private final Point mPosition = new Point();
    @NonNull
    private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new);
    private final boolean mCanSkipFirstFrame;
    private final boolean mIsAppAnimation;
    @NonNull
    private final Rect mRootTaskBounds = new Rect();
    private final int mRootTaskClipMode;
    private final Rect mTmpRect = new Rect();
    private final float mWindowCornerRadius;

    public WindowAnimationSpec(@NonNull Animation animation, Point position,
            boolean canSkipFirstFrame, float windowCornerRadius)  {
        this(animation, position, null /* rootTaskBounds */, canSkipFirstFrame, ROOT_TASK_CLIP_NONE,
                false /* isAppAnimation */, windowCornerRadius);
    }

    public WindowAnimationSpec(@NonNull Animation animation, @Nullable Point position,
            @Nullable Rect rootTaskBounds, boolean canSkipFirstFrame, int rootTaskClipMode,
            boolean isAppAnimation, float windowCornerRadius) {
        mAnimation = animation;
        if (position != null) {
            mPosition.set(position.x, position.y);
        }
        mWindowCornerRadius = windowCornerRadius;
        mCanSkipFirstFrame = canSkipFirstFrame;
        mIsAppAnimation = isAppAnimation;
        mRootTaskClipMode = rootTaskClipMode;
        if (rootTaskBounds != null) {
            mRootTaskBounds.set(rootTaskBounds);
        }
    }

    @NonNull
    @Override
    public WindowAnimationSpec asWindowAnimationSpec() {
        return this;
    }

    @Override
    public boolean getShowWallpaper() {
        return mAnimation.getShowWallpaper();
    }

    @Override
    public boolean getShowBackground() {
        return mAnimation.getShowBackdrop();
    }

    @ColorInt
    @Override
    public int getBackgroundColor() {
        return mAnimation.getBackdropColor();
    }

    /**
     * @return If a window animation has outsets applied to it.
     * @see Animation#getExtensionEdges()
     */
    public boolean hasExtension() {
        return mAnimation.getExtensionEdges() != 0;
    }

    @Override
    public long getDuration() {
        return mAnimation.computeDurationHint();
    }

    @NonNull
    public Rect getRootTaskBounds() {
        return mRootTaskBounds;
    }

    @NonNull
    public Animation getAnimation() {
        return mAnimation;
    }

    @Override
    public void apply(@NonNull Transaction t, @NonNull SurfaceControl leash, long currentPlayTime) {
        final TmpValues tmp = mThreadLocalTmps.get();
        tmp.transformation.clear();
        mAnimation.getTransformation(currentPlayTime, tmp.transformation);
        tmp.transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
        t.setMatrix(leash, tmp.transformation.getMatrix(), tmp.floats);
        t.setAlpha(leash, tmp.transformation.getAlpha());

        boolean cropSet = false;
        if (mRootTaskClipMode == ROOT_TASK_CLIP_NONE) {
            if (tmp.transformation.hasClipRect()) {
                final Rect clipRect = tmp.transformation.getClipRect();
                accountForExtension(tmp.transformation, clipRect);
                t.setWindowCrop(leash, clipRect);
                cropSet = true;
            }
        } else {
            mTmpRect.set(mRootTaskBounds);
            if (tmp.transformation.hasClipRect()) {
                mTmpRect.intersect(tmp.transformation.getClipRect());
            }
            accountForExtension(tmp.transformation, mTmpRect);
            t.setWindowCrop(leash, mTmpRect);
            cropSet = true;
        }

        // We can only apply rounded corner if a crop is set, as otherwise the value is meaningless,
        // since it doesn't have anything it's relative to.
        if (cropSet && mAnimation.hasRoundedCorners() && mWindowCornerRadius > 0) {
            t.setCornerRadius(leash, mWindowCornerRadius);
        }
    }

    private void accountForExtension(Transformation transformation, Rect clipRect) {
        Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
        if (!extensionInsets.equals(Insets.NONE)) {
            // Extend the surface to allow for the edge extension to be visible
            clipRect.inset(extensionInsets);
        }
    }

    @Override
    public long calculateStatusBarTransitionStartTime() {
        TranslateAnimation openTranslateAnimation = findTranslateAnimation(mAnimation);

        if (openTranslateAnimation != null) {
            if (openTranslateAnimation.isXAxisTransition()
                    && openTranslateAnimation.isFullWidthTranslate()) {
                // On X axis transitions that are fullscreen (heuristic for task like transitions)
                // we want the status bar to animate right in the middle of the translation when
                // the windows/tasks have each moved half way across.
                float t = findMiddleOfTranslationFraction(openTranslateAnimation.getInterpolator());

                return SystemClock.uptimeMillis()
                        + openTranslateAnimation.getStartOffset()
                        + (long) (openTranslateAnimation.getDuration() * t)
                        - (long) (STATUS_BAR_TRANSITION_DURATION * 0.5);
            } else {
                // Some interpolators are extremely quickly mostly finished, but not completely. For
                // our purposes, we need to find the fraction for which their interpolator is mostly
                // there, and use that value for the calculation.
                float t = findAlmostThereFraction(openTranslateAnimation.getInterpolator());

                return SystemClock.uptimeMillis()
                        + openTranslateAnimation.getStartOffset()
                        + (long) (openTranslateAnimation.getDuration() * t)
                        - STATUS_BAR_TRANSITION_DURATION;
            }
        } else {
            return SystemClock.uptimeMillis();
        }
    }

    @Override
    public boolean canSkipFirstFrame() {
        return mCanSkipFirstFrame;
    }

    @Override
    public boolean needsEarlyWakeup() {
        return mIsAppAnimation;
    }

    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
        pw.print(prefix); pw.println(mAnimation);
    }

    @Override
    public void dumpDebugInner(@NonNull ProtoOutputStream proto) {
        final long token = proto.start(WINDOW);
        proto.write(ANIMATION, mAnimation.toString());
        proto.end(token);
    }

    /**
     * Tries to find a {@link TranslateAnimation} inside the {@code animation}.
     *
     * @return the found animation, {@code null} otherwise
     */
    @Nullable
    private static TranslateAnimation findTranslateAnimation(@NonNull Animation animation) {
        if (animation instanceof TranslateAnimation) {
            return (TranslateAnimation) animation;
        } else if (animation instanceof AnimationSet) {
            AnimationSet set = (AnimationSet) animation;
            for (int i = 0; i < set.getAnimations().size(); i++) {
                Animation a = set.getAnimations().get(i);
                if (a instanceof TranslateAnimation) {
                    return (TranslateAnimation) a;
                }
            }
        }
        return null;
    }

    /**
     * Finds the fraction of the animation's duration at which the transition is almost done with a
     * maximal error of 0.01 when it is animated with {@code interpolator}.
     */
    private static float findAlmostThereFraction(@NonNull Interpolator interpolator) {
        return findInterpolationAdjustedTargetFraction(interpolator, 0.99f, 0.01f);
    }

    /**
     * Finds the fraction of the animation's duration at which the transition is spatially half way
     * done with a maximal error of 0.01 when it is animated with {@code interpolator}.
     */
    private float findMiddleOfTranslationFraction(@NonNull Interpolator interpolator) {
        return findInterpolationAdjustedTargetFraction(interpolator, 0.5f, 0.01f);
    }

    /**
     * Binary searches for a {@code val} such that there exists an {@code -0.01 < epsilon < 0.01}
     * for which {@code interpolator(val + epsilon) > target}.
     */
    private static float findInterpolationAdjustedTargetFraction(@NonNull Interpolator interpolator,
            float target, float epsilon) {
        float val = 0.5f;
        float adj = 0.25f;

        while (adj >= epsilon) {
            if (interpolator.getInterpolation(val) < target) {
                val += adj;
            } else {
                val -= adj;
            }
            adj /= 2;
        }

        return val;
    }

    private static final class TmpValues {
        @NonNull
        final Transformation transformation = new Transformation();
        @NonNull
        final float[] floats = new float[9];
    }
}
