/*
 * 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.DimenRes;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RecordingCanvas;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Trace;
import android.view.RenderNodeAnimator;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;

import java.util.ArrayList;
import java.util.HashSet;

final class KeyButtonRipple extends Drawable {

    private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
    private static final float GLOW_MAX_ALPHA = 0.2f;
    private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
    private static final int ANIMATION_DURATION_SCALE = 350;
    private static final int ANIMATION_DURATION_FADE = 450;
    private static final Interpolator ALPHA_OUT_INTERPOLATOR =
            new PathInterpolator(0f, 0f, 0.8f, 1f);

    @DimenRes
    private final int mMaxWidthResource;

    private Paint mRipplePaint;
    private CanvasProperty<Float> mLeftProp;
    private CanvasProperty<Float> mTopProp;
    private CanvasProperty<Float> mRightProp;
    private CanvasProperty<Float> mBottomProp;
    private CanvasProperty<Float> mRxProp;
    private CanvasProperty<Float> mRyProp;
    private CanvasProperty<Paint> mPaintProp;
    private float mGlowAlpha = 0f;
    private float mGlowScale = 1f;
    private boolean mPressed;
    private boolean mVisible;
    private boolean mDrawingHardwareGlow;
    private int mMaxWidth;
    private boolean mLastDark;
    private boolean mDark;
    private boolean mDelayTouchFeedback;

    private final Interpolator mInterpolator = new LogInterpolator();
    private boolean mSupportHardware;
    private final View mTargetView;
    private final Handler mHandler = new Handler();

    private final HashSet<Animator> mRunningAnimations = new HashSet<>();
    private final ArrayList<Animator> mTmpArray = new ArrayList<>();

    private final TraceAnimatorListener mExitHwTraceAnimator =
            new TraceAnimatorListener("exitHardware");
    private final TraceAnimatorListener mEnterHwTraceAnimator =
            new TraceAnimatorListener("enterHardware");

    public enum Type {
        OVAL,
        ROUNDED_RECT
    }

    private Type mType = Type.ROUNDED_RECT;

    KeyButtonRipple(Context ctx, View targetView, @DimenRes int maxWidthResource) {
        mMaxWidthResource = maxWidthResource;
        mMaxWidth = ctx.getResources().getDimensionPixelSize(maxWidthResource);
        mTargetView = targetView;
    }

    public void updateResources() {
        mMaxWidth = mTargetView.getContext().getResources()
                .getDimensionPixelSize(mMaxWidthResource);
        invalidateSelf();
    }

    public void setDarkIntensity(float darkIntensity) {
        mDark = darkIntensity >= 0.5f;
    }

    public void setDelayTouchFeedback(boolean delay) {
        mDelayTouchFeedback = delay;
    }

    public void setType(Type type) {
        mType = type;
    }

    private Paint getRipplePaint() {
        if (mRipplePaint == null) {
            mRipplePaint = new Paint();
            mRipplePaint.setAntiAlias(true);
            mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
        }
        return mRipplePaint;
    }

    private void drawSoftware(Canvas canvas) {
        if (mGlowAlpha > 0f) {
            final Paint p = getRipplePaint();
            p.setAlpha((int) (mGlowAlpha * 255f));

            final float w = getBounds().width();
            final float h = getBounds().height();
            final boolean horizontal = w > h;
            final float diameter = getRippleSize() * mGlowScale;
            final float radius = diameter * .5f;
            final float cx = w * .5f;
            final float cy = h * .5f;
            final float rx = horizontal ? radius : cx;
            final float ry = horizontal ? cy : radius;
            final float corner = horizontal ? cy : cx;

            if (mType == Type.ROUNDED_RECT) {
                canvas.drawRoundRect(cx - rx, cy - ry, cx + rx, cy + ry, corner, corner, p);
            } else {
                canvas.save();
                canvas.translate(cx, cy);
                float r = Math.min(rx, ry);
                canvas.drawOval(-r, -r, r, r, p);
                canvas.restore();
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        mSupportHardware = canvas.isHardwareAccelerated();
        if (mSupportHardware) {
            drawHardware((RecordingCanvas) canvas);
        } else {
            drawSoftware(canvas);
        }
    }

    @Override
    public void setAlpha(int alpha) {
        // Not supported.
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        // Not supported.
    }

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

    private boolean isHorizontal() {
        return getBounds().width() > getBounds().height();
    }

    private void drawHardware(RecordingCanvas c) {
        if (mDrawingHardwareGlow) {
            if (mType == Type.ROUNDED_RECT) {
                c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
                        mPaintProp);
            } else {
                CanvasProperty<Float> cx = CanvasProperty.createFloat(getBounds().width() / 2);
                CanvasProperty<Float> cy = CanvasProperty.createFloat(getBounds().height() / 2);
                int d = Math.min(getBounds().width(), getBounds().height());
                CanvasProperty<Float> r = CanvasProperty.createFloat(1.0f * d / 2);
                c.drawCircle(cx, cy, r, mPaintProp);
            }
        }
    }

    /** Gets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
    public float getGlowAlpha() {
        return mGlowAlpha;
    }

    /** Sets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
    public void setGlowAlpha(float x) {
        mGlowAlpha = x;
        invalidateSelf();
    }

    /** Gets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
    public float getGlowScale() {
        return mGlowScale;
    }

    /** Sets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
    public void setGlowScale(float x) {
        mGlowScale = x;
        invalidateSelf();
    }

    private float getMaxGlowAlpha() {
        return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
    }

    @Override
    protected boolean onStateChange(int[] state) {
        boolean pressed = false;
        for (int i = 0; i < state.length; i++) {
            if (state[i] == android.R.attr.state_pressed) {
                pressed = true;
                break;
            }
        }
        if (pressed != mPressed) {
            setPressed(pressed);
            mPressed = pressed;
            return true;
        } else {
            return false;
        }
    }

    @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() {
        endAnimations("jumpToCurrentState", false /* cancel */);
    }

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

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

    public void setPressed(boolean pressed) {
        if (mDark != mLastDark && pressed) {
            mRipplePaint = null;
            mLastDark = mDark;
        }
        if (mSupportHardware) {
            setPressedHardware(pressed);
        } else {
            setPressedSoftware(pressed);
        }
    }

    /**
     * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
     * is enabled.
     */
    public void abortDelayedRipple() {
        mHandler.removeCallbacksAndMessages(null);
    }

    private void endAnimations(String reason, boolean cancel) {
        Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
        Trace.endSection();
        mVisible = false;
        mTmpArray.addAll(mRunningAnimations);
        int size = mTmpArray.size();
        for (int i = 0; i < size; i++) {
            Animator a = mTmpArray.get(i);
            if (cancel) {
                a.cancel();
            } else {
                a.end();
            }
        }
        mTmpArray.clear();
        mRunningAnimations.clear();
        mHandler.removeCallbacksAndMessages(null);
    }

    private void setPressedSoftware(boolean pressed) {
        if (pressed) {
            if (mDelayTouchFeedback) {
                if (mRunningAnimations.isEmpty()) {
                    mHandler.removeCallbacksAndMessages(null);
                    mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
                } else if (mVisible) {
                    enterSoftware();
                }
            } else {
                enterSoftware();
            }
        } else {
            exitSoftware();
        }
    }

    private void enterSoftware() {
        endAnimations("enterSoftware", true /* cancel */);
        mVisible = true;
        mGlowAlpha = getMaxGlowAlpha();
        ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
                0f, GLOW_MAX_SCALE_FACTOR);
        scaleAnimator.setInterpolator(mInterpolator);
        scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
        scaleAnimator.addListener(mAnimatorListener);
        scaleAnimator.start();
        mRunningAnimations.add(scaleAnimator);

        // With the delay, it could eventually animate the enter animation with no pressed state,
        // then immediately show the exit animation. If this is skipped there will be no ripple.
        if (mDelayTouchFeedback && !mPressed) {
            exitSoftware();
        }
    }

    private void exitSoftware() {
        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
        alphaAnimator.setInterpolator(ALPHA_OUT_INTERPOLATOR);
        alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
        alphaAnimator.addListener(mAnimatorListener);
        alphaAnimator.start();
        mRunningAnimations.add(alphaAnimator);
    }

    private void setPressedHardware(boolean pressed) {
        if (pressed) {
            if (mDelayTouchFeedback) {
                if (mRunningAnimations.isEmpty()) {
                    mHandler.removeCallbacksAndMessages(null);
                    mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
                } else if (mVisible) {
                    enterHardware();
                }
            } else {
                enterHardware();
            }
        } else {
            exitHardware();
        }
    }

    /**
     * Sets the left/top property for the round rect to {@code prop} depending on whether we are
     * horizontal or vertical mode.
     */
    private void setExtendStart(CanvasProperty<Float> prop) {
        if (isHorizontal()) {
            mLeftProp = prop;
        } else {
            mTopProp = prop;
        }
    }

    private CanvasProperty<Float> getExtendStart() {
        return isHorizontal() ? mLeftProp : mTopProp;
    }

    /**
     * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
     * horizontal or vertical mode.
     */
    private void setExtendEnd(CanvasProperty<Float> prop) {
        if (isHorizontal()) {
            mRightProp = prop;
        } else {
            mBottomProp = prop;
        }
    }

    private CanvasProperty<Float> getExtendEnd() {
        return isHorizontal() ? mRightProp : mBottomProp;
    }

    private int getExtendSize() {
        return isHorizontal() ? getBounds().width() : getBounds().height();
    }

    private int getRippleSize() {
        int size = isHorizontal() ? getBounds().width() : getBounds().height();
        return Math.min(size, mMaxWidth);
    }

    private void enterHardware() {
        endAnimations("enterHardware", true /* cancel */);
        mVisible = true;
        mDrawingHardwareGlow = true;
        setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
        final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
                getExtendSize() / 2 - GLOW_MAX_SCALE_FACTOR * getRippleSize() / 2);
        startAnim.setDuration(ANIMATION_DURATION_SCALE);
        startAnim.setInterpolator(mInterpolator);
        startAnim.addListener(mAnimatorListener);
        startAnim.setTarget(mTargetView);

        setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
        final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
                getExtendSize() / 2 + GLOW_MAX_SCALE_FACTOR * getRippleSize() / 2);
        endAnim.setDuration(ANIMATION_DURATION_SCALE);
        endAnim.setInterpolator(mInterpolator);
        endAnim.addListener(mAnimatorListener);
        endAnim.addListener(mEnterHwTraceAnimator);
        endAnim.setTarget(mTargetView);

        if (isHorizontal()) {
            mTopProp = CanvasProperty.createFloat(0f);
            mBottomProp = CanvasProperty.createFloat(getBounds().height());
            mRxProp = CanvasProperty.createFloat(getBounds().height() / 2);
            mRyProp = CanvasProperty.createFloat(getBounds().height() / 2);
        } else {
            mLeftProp = CanvasProperty.createFloat(0f);
            mRightProp = CanvasProperty.createFloat(getBounds().width());
            mRxProp = CanvasProperty.createFloat(getBounds().width() / 2);
            mRyProp = CanvasProperty.createFloat(getBounds().width() / 2);
        }

        mGlowScale = GLOW_MAX_SCALE_FACTOR;
        mGlowAlpha = getMaxGlowAlpha();
        mRipplePaint = getRipplePaint();
        mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
        mPaintProp = CanvasProperty.createPaint(mRipplePaint);

        startAnim.start();
        endAnim.start();
        mRunningAnimations.add(startAnim);
        mRunningAnimations.add(endAnim);

        invalidateSelf();

        // With the delay, it could eventually animate the enter animation with no pressed state,
        // then immediately show the exit animation. If this is skipped there will be no ripple.
        if (mDelayTouchFeedback && !mPressed) {
            exitHardware();
        }
    }

    private void exitHardware() {
        mPaintProp = CanvasProperty.createPaint(getRipplePaint());
        final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
                RenderNodeAnimator.PAINT_ALPHA, 0);
        opacityAnim.setDuration(ANIMATION_DURATION_FADE);
        opacityAnim.setInterpolator(ALPHA_OUT_INTERPOLATOR);
        opacityAnim.addListener(mAnimatorListener);
        opacityAnim.addListener(mExitHwTraceAnimator);
        opacityAnim.setTarget(mTargetView);

        opacityAnim.start();
        mRunningAnimations.add(opacityAnim);

        invalidateSelf();
    }

    private final AnimatorListenerAdapter mAnimatorListener =
            new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mRunningAnimations.remove(animation);
                    if (mRunningAnimations.isEmpty() && !mPressed) {
                        mVisible = false;
                        mDrawingHardwareGlow = false;
                        invalidateSelf();
                    }
                }
            };

    private static final class TraceAnimatorListener extends AnimatorListenerAdapter {
        private final String mName;
        TraceAnimatorListener(String name) {
            mName = name;
        }

        @Override
        public void onAnimationStart(Animator animation) {
            Trace.beginSection("KeyButtonRipple.start." + mName);
            Trace.endSection();
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            Trace.beginSection("KeyButtonRipple.cancel." + mName);
            Trace.endSection();
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            Trace.beginSection("KeyButtonRipple.end." + mName);
            Trace.endSection();
        }
    }

    /**
     * Interpolator with a smooth log deceleration
     */
    private static final class LogInterpolator implements Interpolator {
        @Override
        public float getInterpolation(float input) {
            return 1 - (float) Math.pow(400, -input * 1.4);
        }
    }
}
