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

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;
import android.view.animation.DecelerateInterpolator;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;

/**
 * Drawable used on SysUI scrims.
 */
public class ScrimDrawable extends Drawable {
    private static final String TAG = "ScrimDrawable";

    private boolean mShouldUseLargeScreenSize;
    private final Paint mPaint;
    private final Path mPath = new Path();
    private final RectF mBoundsRectF = new RectF();

    private int mAlpha = 255;
    private int mMainColor;
    private ValueAnimator mColorAnimation;
    private int mMainColorTo;
    private float mCornerRadius;
    private ConcaveInfo mConcaveInfo;
    private int mBottomEdgePosition;
    private float mBottomEdgeRadius = -1;
    private boolean mCornerRadiusEnabled;

    public ScrimDrawable() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mShouldUseLargeScreenSize = false;
    }

    /**
     * Sets the background color.
     * @param mainColor the color.
     * @param animated if transition should be interpolated.
     */
    public void setColor(int mainColor, boolean animated) {
        if (mainColor == mMainColorTo) {
            return;
        }

        if (mColorAnimation != null && mColorAnimation.isRunning()) {
            mColorAnimation.cancel();
        }

        mMainColorTo = mainColor;

        if (animated) {
            final int mainFrom = mMainColor;

            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
            anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
            anim.addUpdateListener(animation -> {
                float ratio = (float) animation.getAnimatedValue();
                mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio);
                invalidateSelf();
            });
            anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
                    if (mColorAnimation == animation) {
                        mColorAnimation = null;
                    }
                }
            });
            anim.setInterpolator(new DecelerateInterpolator());
            anim.start();
            mColorAnimation = anim;
        } else {
            mMainColor = mainColor;
            invalidateSelf();
        }
    }

    @Override
    public void setAlpha(int alpha) {
        if (alpha != mAlpha) {
            mAlpha = alpha;
            invalidateSelf();
        }
    }

    @Override
    public int getAlpha() {
        return mAlpha;
    }

    @Override
    public void setXfermode(@Nullable Xfermode mode) {
        mPaint.setXfermode(mode);
        invalidateSelf();
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public ColorFilter getColorFilter() {
        return mPaint.getColorFilter();
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    public void setShouldUseLargeScreenSize(boolean v) {
        mShouldUseLargeScreenSize = v;
    }

    /**
     * Corner radius used by either concave or convex corners.
     */
    public void setRoundedCorners(float radius) {
        if (radius == mCornerRadius) {
            return;
        }
        mCornerRadius = radius;
        if (mConcaveInfo != null) {
            mConcaveInfo.setCornerRadius(radius);
            updatePath();
        }
        invalidateSelf();
    }

    /**
     * If we should draw a rounded rect instead of a rect.
     */
    public void setRoundedCornersEnabled(boolean enabled) {
        if (mCornerRadiusEnabled == enabled) {
            return;
        }
        mCornerRadiusEnabled = enabled;
        invalidateSelf();
    }

    /**
     * If we should draw a concave rounded rect instead of a rect.
     */
    public void setBottomEdgeConcave(boolean enabled) {
        if (enabled && mConcaveInfo != null) {
            return;
        }
        if (!enabled) {
            mConcaveInfo = null;
        } else {
            mConcaveInfo = new ConcaveInfo();
            mConcaveInfo.setCornerRadius(mCornerRadius);
        }
        invalidateSelf();
    }

    /**
     * Location of concave edge.
     * @see #setBottomEdgeConcave(boolean)
     */
    public void setBottomEdgePosition(int y) {
        if (mBottomEdgePosition == y) {
            return;
        }
        mBottomEdgePosition = y;
        if (mConcaveInfo == null) {
            return;
        }
        updatePath();
        invalidateSelf();
    }

    public void setBottomEdgeRadius(float radius) {
        if (mBottomEdgeRadius != radius) {
            mBottomEdgeRadius = radius;
            invalidateSelf();
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        mPaint.setColor(mMainColor);
        mPaint.setAlpha(mAlpha);
        if (mConcaveInfo != null) {
            drawConcave(canvas);
        } else if (mCornerRadiusEnabled && mCornerRadius > 0) {
            float topEdgeRadius = mCornerRadius;
            float bottomEdgeRadius = mBottomEdgeRadius == -1.0 ? mCornerRadius : mBottomEdgeRadius;

            mBoundsRectF.set(getBounds());

            // When the back gesture causes the notification scrim to be scaled down,
            // this offset "reveals" the rounded bottom edge as it "pulls away".
            // We must *not* make this adjustment on largescreen shades (where the corner is sharp).
            if (!mShouldUseLargeScreenSize && mBottomEdgeRadius != -1) {
                mBoundsRectF.bottom -= bottomEdgeRadius;
            }

            // We need a box with rounded corners but its lower corners are not rounded on large
            // screen devices in "portrait" orientation.
            // Thus, we cannot draw a symmetric rounded rectangle via canvas.drawRoundRect()
            // and must build a box with different corner radii at the top and at the bottom.
            // Additionally, when the scrim is pushed to the very bottom of the screen, do not draw
            // anything (drawing a rounded box with these specifications is not possible).
            // TODO(b/271030611) perhaps this could be accomplished via Path.addRoundRect instead?
            if (mBoundsRectF.bottom - mBoundsRectF.top > bottomEdgeRadius) {
                mPath.reset();
                mPath.moveTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius);
                mPath.cubicTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius,
                        mBoundsRectF.right, mBoundsRectF.top,
                        mBoundsRectF.right - topEdgeRadius, mBoundsRectF.top);
                mPath.lineTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top);
                mPath.cubicTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top,
                        mBoundsRectF.left, mBoundsRectF.top,
                        mBoundsRectF.left, mBoundsRectF.top + topEdgeRadius);
                mPath.lineTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius);
                mPath.cubicTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius,
                        mBoundsRectF.left, mBoundsRectF.bottom,
                        mBoundsRectF.left + bottomEdgeRadius, mBoundsRectF.bottom);
                mPath.lineTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom);
                mPath.cubicTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom,
                        mBoundsRectF.right, mBoundsRectF.bottom,
                        mBoundsRectF.right, mBoundsRectF.bottom - bottomEdgeRadius);
                mPath.close();
                canvas.drawPath(mPath, mPaint);
            }
        } else {
            canvas.drawRect(getBounds().left, getBounds().top, getBounds().right,
                    getBounds().bottom, mPaint);
        }
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        updatePath();
    }

    private void drawConcave(Canvas canvas) {
        canvas.clipOutPath(mConcaveInfo.mPath);
        canvas.drawRect(getBounds().left, getBounds().top, getBounds().right,
                mBottomEdgePosition + mConcaveInfo.mPathOverlap, mPaint);
    }

    private void updatePath() {
        if (mConcaveInfo == null) {
            return;
        }
        mConcaveInfo.mPath.reset();
        float top = mBottomEdgePosition;
        float bottom = mBottomEdgePosition + mConcaveInfo.mPathOverlap;
        mConcaveInfo.mPath.addRoundRect(getBounds().left, top, getBounds().right, bottom,
                mConcaveInfo.mCornerRadii, Path.Direction.CW);
    }

    @VisibleForTesting
    public int getMainColor() {
        return mMainColor;
    }

    private static class ConcaveInfo {
        private float mPathOverlap;
        private final float[] mCornerRadii;
        private final Path mPath = new Path();

        ConcaveInfo() {
            mCornerRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0};
        }

        public void setCornerRadius(float radius) {
            mPathOverlap = radius;
            mCornerRadii[0] = radius;
            mCornerRadii[1] = radius;
            mCornerRadii[2] = radius;
            mCornerRadii[3] = radius;
        }
    }
}
