/*
 * Copyright (C) 2022 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 android.inputmethodservice.navigationbar;

import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_COLOR;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_OFFSET_X;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_OFFSET_Y;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_RADIUS;
import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx;

import android.animation.ArgbEvaluator;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlurMaskFilter;
import android.graphics.BlurMaskFilter.Blur;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.view.View;


/**
 * Drawable for {@link KeyButtonView}s that supports tinting between two colors, rotation and shows
 * a shadow. AnimatedVectorDrawable will only support tinting from intensities but has no support
 * for shadows nor rotations.
 */
final class KeyButtonDrawable extends Drawable {

    public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_ROTATE =
            new FloatProperty<KeyButtonDrawable>("KeyButtonRotation") {
                @Override
                public void setValue(KeyButtonDrawable drawable, float degree) {
                    drawable.setRotation(degree);
                }

                @Override
                public Float get(KeyButtonDrawable drawable) {
                    return drawable.getRotation();
                }
            };

    public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_TRANSLATE_Y =
            new FloatProperty<KeyButtonDrawable>("KeyButtonTranslateY") {
                @Override
                public void setValue(KeyButtonDrawable drawable, float y) {
                    drawable.setTranslationY(y);
                }

                @Override
                public Float get(KeyButtonDrawable drawable) {
                    return drawable.getTranslationY();
                }
            };

    private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    private final ShadowDrawableState mState;
    private AnimatedVectorDrawable mAnimatedDrawable;
    private final Callback mAnimatedDrawableCallback = new Callback() {
        @Override
        public void invalidateDrawable(@NonNull Drawable who) {
            invalidateSelf();
        }

        @Override
        public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
            scheduleSelf(what, when);
        }

        @Override
        public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
            unscheduleSelf(what);
        }
    };

    KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor,
            boolean horizontalFlip, Color ovalBackgroundColor) {
        this(d, new ShadowDrawableState(lightColor, darkColor,
                d instanceof AnimatedVectorDrawable, horizontalFlip, ovalBackgroundColor));
    }

    private KeyButtonDrawable(Drawable d, ShadowDrawableState state) {
        mState = state;
        if (d != null) {
            mState.mBaseHeight = d.getIntrinsicHeight();
            mState.mBaseWidth = d.getIntrinsicWidth();
            mState.mChangingConfigurations = d.getChangingConfigurations();
            mState.mChildState = d.getConstantState();
        }
        if (canAnimate()) {
            mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate();
            mAnimatedDrawable.setCallback(mAnimatedDrawableCallback);
            setDrawableBounds(mAnimatedDrawable);
        }
    }

    public void setDarkIntensity(float intensity) {
        mState.mDarkIntensity = intensity;
        final int color = (int) ArgbEvaluator.getInstance()
                .evaluate(intensity, mState.mLightColor, mState.mDarkColor);
        updateShadowAlpha();
        setColorFilter(new PorterDuffColorFilter(color, Mode.SRC_ATOP));
    }

    public void setRotation(float degrees) {
        if (canAnimate()) {
            // AnimatedVectorDrawables will not support rotation
            return;
        }
        if (mState.mRotateDegrees != degrees) {
            mState.mRotateDegrees = degrees;
            invalidateSelf();
        }
    }

    public void setTranslationX(float x) {
        setTranslation(x, mState.mTranslationY);
    }

    public void setTranslationY(float y) {
        setTranslation(mState.mTranslationX, y);
    }

    public void setTranslation(float x, float y) {
        if (mState.mTranslationX != x || mState.mTranslationY != y) {
            mState.mTranslationX = x;
            mState.mTranslationY = y;
            invalidateSelf();
        }
    }

    public void setShadowProperties(int x, int y, int size, int color) {
        if (canAnimate()) {
            // AnimatedVectorDrawables will not support shadows
            return;
        }
        if (mState.mShadowOffsetX != x || mState.mShadowOffsetY != y
                || mState.mShadowSize != size || mState.mShadowColor != color) {
            mState.mShadowOffsetX = x;
            mState.mShadowOffsetY = y;
            mState.mShadowSize = size;
            mState.mShadowColor = color;
            mShadowPaint.setColorFilter(
                    new PorterDuffColorFilter(mState.mShadowColor, Mode.SRC_ATOP));
            updateShadowAlpha();
            invalidateSelf();
        }
    }

    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        boolean changed = super.setVisible(visible, restart);
        if (changed) {
            // End any existing animations when the visibility changes
            jumpToCurrentState();
        }
        return changed;
    }

    @Override
    public void jumpToCurrentState() {
        super.jumpToCurrentState();
        if (mAnimatedDrawable != null) {
            mAnimatedDrawable.jumpToCurrentState();
        }
    }

    @Override
    public void setAlpha(int alpha) {
        mState.mAlpha = alpha;
        mIconPaint.setAlpha(alpha);
        updateShadowAlpha();
        invalidateSelf();
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mIconPaint.setColorFilter(colorFilter);
        if (mAnimatedDrawable != null) {
            if (hasOvalBg()) {
                mAnimatedDrawable.setColorFilter(
                        new PorterDuffColorFilter(mState.mLightColor, PorterDuff.Mode.SRC_IN));
            } else {
                mAnimatedDrawable.setColorFilter(colorFilter);
            }
        }
        invalidateSelf();
    }

    public float getDarkIntensity() {
        return mState.mDarkIntensity;
    }

    public float getRotation() {
        return mState.mRotateDegrees;
    }

    public float getTranslationX() {
        return mState.mTranslationX;
    }

    public float getTranslationY() {
        return mState.mTranslationY;
    }

    @Override
    public ConstantState getConstantState() {
        return mState;
    }

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

    @Override
    public int getIntrinsicHeight() {
        return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2;
    }

    @Override
    public int getIntrinsicWidth() {
        return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2;
    }

    public boolean canAnimate() {
        return mState.mSupportsAnimation;
    }

    public void startAnimation() {
        if (mAnimatedDrawable != null) {
            mAnimatedDrawable.start();
        }
    }

    public void resetAnimation() {
        if (mAnimatedDrawable != null) {
            mAnimatedDrawable.reset();
        }
    }

    public void clearAnimationCallbacks() {
        if (mAnimatedDrawable != null) {
            mAnimatedDrawable.clearAnimationCallbacks();
        }
    }

    @Override
    public void draw(Canvas canvas) {
        Rect bounds = getBounds();
        if (bounds.isEmpty()) {
            return;
        }

        if (mAnimatedDrawable != null) {
            mAnimatedDrawable.draw(canvas);
        } else {
            // If no cache or previous cached bitmap is hardware/software acceleration does not
            // match the current canvas on draw then regenerate
            boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated();
            if (hwBitmapChanged) {
                mState.mIsHardwareBitmap = canvas.isHardwareAccelerated();
            }
            if (mState.mLastDrawnIcon == null || hwBitmapChanged) {
                regenerateBitmapIconCache();
            }
            canvas.save();
            canvas.translate(mState.mTranslationX, mState.mTranslationY);
            canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);

            if (mState.mShadowSize > 0) {
                if (mState.mLastDrawnShadow == null || hwBitmapChanged) {
                    regenerateBitmapShadowCache();
                }

                // Translate (with rotation offset) before drawing the shadow
                final float radians = (float) (mState.mRotateDegrees * Math.PI / 180);
                final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY
                        + Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX;
                final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY
                        - Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY;
                canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY,
                        mShadowPaint);
            }
            canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint);
            canvas.restore();
        }
    }

    @Override
    public boolean canApplyTheme() {
        return mState.canApplyTheme();
    }

    @ColorInt int getDrawableBackgroundColor() {
        return mState.mOvalBackgroundColor.toArgb();
    }

    boolean hasOvalBg() {
        return mState.mOvalBackgroundColor != null;
    }

    private void regenerateBitmapIconCache() {
        final int width = getIntrinsicWidth();
        final int height = getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(bitmap);

        // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
        final Drawable d = mState.mChildState.newDrawable().mutate();
        setDrawableBounds(d);
        canvas.save();
        if (mState.mHorizontalFlip) {
            canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
        }
        d.draw(canvas);
        canvas.restore();

        if (mState.mIsHardwareBitmap) {
            bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
        }
        mState.mLastDrawnIcon = bitmap;
    }

    private void regenerateBitmapShadowCache() {
        if (mState.mShadowSize == 0) {
            // No shadow
            mState.mLastDrawnIcon = null;
            return;
        }

        final int width = getIntrinsicWidth();
        final int height = getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
        final Drawable d = mState.mChildState.newDrawable().mutate();
        setDrawableBounds(d);
        canvas.save();
        if (mState.mHorizontalFlip) {
            canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
        }
        d.draw(canvas);
        canvas.restore();

        // Draws the shadow from original drawable
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL));
        int[] offset = new int[2];
        final Bitmap shadow = bitmap.extractAlpha(paint, offset);
        paint.setMaskFilter(null);
        bitmap.eraseColor(Color.TRANSPARENT);
        canvas.drawBitmap(shadow, offset[0], offset[1], paint);

        if (mState.mIsHardwareBitmap) {
            bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
        }
        mState.mLastDrawnShadow = bitmap;
    }

    /**
     * Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since
     * dark color and shadow should not be visible at the same time.
     */
    private void updateShadowAlpha() {
        // Update the color from the original color's alpha as the max
        int alpha = Color.alpha(mState.mShadowColor);
        mShadowPaint.setAlpha(
                Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity)));
    }

    /**
     * Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset
     * @param d the drawable to set the bounds
     */
    private void setDrawableBounds(Drawable d) {
        final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX);
        final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY);
        d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX,
                getIntrinsicHeight() - offsetY);
    }

    private static class ShadowDrawableState extends ConstantState {
        int mChangingConfigurations;
        int mBaseWidth;
        int mBaseHeight;
        float mRotateDegrees;
        float mTranslationX;
        float mTranslationY;
        int mShadowOffsetX;
        int mShadowOffsetY;
        int mShadowSize;
        int mShadowColor;
        float mDarkIntensity;
        int mAlpha;
        boolean mHorizontalFlip;

        boolean mIsHardwareBitmap;
        Bitmap mLastDrawnIcon;
        Bitmap mLastDrawnShadow;
        ConstantState mChildState;

        final int mLightColor;
        final int mDarkColor;
        final boolean mSupportsAnimation;
        final Color mOvalBackgroundColor;

        ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor, boolean animated,
                boolean horizontalFlip, Color ovalBackgroundColor) {
            mLightColor = lightColor;
            mDarkColor = darkColor;
            mSupportsAnimation = animated;
            mAlpha = 255;
            mHorizontalFlip = horizontalFlip;
            mOvalBackgroundColor = ovalBackgroundColor;
        }

        @Override
        public Drawable newDrawable() {
            return new KeyButtonDrawable(null, this);
        }

        @Override
        public int getChangingConfigurations() {
            return mChangingConfigurations;
        }

        @Override
        public boolean canApplyTheme() {
            return true;
        }
    }

    /**
     * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
     * {@link #create(Context, int, boolean, boolean)}.
     */
    public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
            @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow,
            Color ovalBackgroundColor) {
        final Resources res = context.getResources();
        boolean isRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
        Drawable d = context.getDrawable(iconResId);
        final KeyButtonDrawable drawable = new KeyButtonDrawable(d, lightColor, darkColor,
                isRtl && d.isAutoMirrored(), ovalBackgroundColor);
        if (hasShadow) {
            int offsetX = dpToPx(NAV_KEY_BUTTON_SHADOW_OFFSET_X, res);
            int offsetY = dpToPx(NAV_KEY_BUTTON_SHADOW_OFFSET_Y, res);
            int radius = dpToPx(NAV_KEY_BUTTON_SHADOW_RADIUS, res);
            int color = NAV_KEY_BUTTON_SHADOW_COLOR;
            drawable.setShadowProperties(offsetX, offsetY, radius, color);
        }
        return drawable;
    }
}
