/*
 * 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.surfaceeffects.ripple

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.core.graphics.ColorUtils
import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape

/**
 * A generic expanding ripple effect.
 *
 * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter],
 * then call [startRipple] to trigger the ripple expansion.
 */
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    protected lateinit var rippleShader: RippleShader
    lateinit var rippleShape: RippleShape
        private set

    private val ripplePaint = Paint()
    protected val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)

    var duration: Long = 1750

    fun setMaxSize(maxWidth: Float, maxHeight: Float) {
        rippleShader.rippleSize.setMaxSize(maxWidth, maxHeight)
    }

    private var centerX: Float = 0.0f
    private var centerY: Float = 0.0f
    fun setCenter(x: Float, y: Float) {
        this.centerX = x
        this.centerY = y
        rippleShader.setCenter(x, y)
    }

    override fun onConfigurationChanged(newConfig: Configuration?) {
        rippleShader.pixelDensity = resources.displayMetrics.density
        super.onConfigurationChanged(newConfig)
    }

    override fun onAttachedToWindow() {
        rippleShader.pixelDensity = resources.displayMetrics.density
        super.onAttachedToWindow()
    }

    /** Initializes the shader. Must be called before [startRipple]. */
    fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) {
        this.rippleShape = rippleShape
        rippleShader = RippleShader(rippleShape)

        rippleShader.color = RippleShader.RIPPLE_DEFAULT_COLOR
        rippleShader.rawProgress = 0f
        rippleShader.sparkleStrength = RippleShader.RIPPLE_SPARKLE_STRENGTH
        rippleShader.pixelDensity = resources.displayMetrics.density

        ripplePaint.shader = rippleShader
    }

    /**
     * Sets the fade parameters for the base ring.
     *
     * <p>Base ring indicates a blurred ring below the sparkle ring. See
     * [RippleShader.baseRingFadeParams].
     */
    @JvmOverloads
    fun setBaseRingFadeParams(
        fadeInStart: Float = rippleShader.baseRingFadeParams.fadeInStart,
        fadeInEnd: Float = rippleShader.baseRingFadeParams.fadeInEnd,
        fadeOutStart: Float = rippleShader.baseRingFadeParams.fadeOutStart,
        fadeOutEnd: Float = rippleShader.baseRingFadeParams.fadeOutEnd
    ) {
        setFadeParams(
            rippleShader.baseRingFadeParams,
            fadeInStart,
            fadeInEnd,
            fadeOutStart,
            fadeOutEnd
        )
    }

    /**
     * Sets the fade parameters for the sparkle ring.
     *
     * <p>Sparkle ring refers to the ring that's drawn on top of the base ring. See
     * [RippleShader.sparkleRingFadeParams].
     */
    @JvmOverloads
    fun setSparkleRingFadeParams(
        fadeInStart: Float = rippleShader.sparkleRingFadeParams.fadeInStart,
        fadeInEnd: Float = rippleShader.sparkleRingFadeParams.fadeInEnd,
        fadeOutStart: Float = rippleShader.sparkleRingFadeParams.fadeOutStart,
        fadeOutEnd: Float = rippleShader.sparkleRingFadeParams.fadeOutEnd
    ) {
        setFadeParams(
            rippleShader.sparkleRingFadeParams,
            fadeInStart,
            fadeInEnd,
            fadeOutStart,
            fadeOutEnd
        )
    }

    /**
     * Sets the fade parameters for the center fill.
     *
     * <p>One common use case is set all the params to 1, which completely removes the center fill.
     * See [RippleShader.centerFillFadeParams].
     */
    @JvmOverloads
    fun setCenterFillFadeParams(
        fadeInStart: Float = rippleShader.centerFillFadeParams.fadeInStart,
        fadeInEnd: Float = rippleShader.centerFillFadeParams.fadeInEnd,
        fadeOutStart: Float = rippleShader.centerFillFadeParams.fadeOutStart,
        fadeOutEnd: Float = rippleShader.centerFillFadeParams.fadeOutEnd
    ) {
        setFadeParams(
            rippleShader.centerFillFadeParams,
            fadeInStart,
            fadeInEnd,
            fadeOutStart,
            fadeOutEnd
        )
    }

    private fun setFadeParams(
        fadeParams: RippleShader.FadeParams,
        fadeInStart: Float,
        fadeInEnd: Float,
        fadeOutStart: Float,
        fadeOutEnd: Float
    ) {
        with(fadeParams) {
            this.fadeInStart = fadeInStart
            this.fadeInEnd = fadeInEnd
            this.fadeOutStart = fadeOutStart
            this.fadeOutEnd = fadeOutEnd
        }
    }

    /**
     * Sets blur multiplier at start and end of the progress.
     *
     * <p>It interpolates between [start] and [end]. No need to set it if using default blur.
     */
    fun setBlur(start: Float, end: Float) {
        rippleShader.blurStart = start
        rippleShader.blurEnd = end
    }

    /**
     * Sets the list of [RippleShader.SizeAtProgress].
     *
     * <p>Note that this clears the list before it sets with the new data.
     */
    fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) {
        rippleShader.rippleSize.setSizeAtProgresses(*targetSizes)
    }

    @JvmOverloads
    fun startRipple(onAnimationEnd: Runnable? = null) {
        if (animator.isRunning) {
            return // Ignore if ripple effect is already playing
        }
        animator.duration = duration
        animator.addUpdateListener { updateListener ->
            val now = updateListener.currentPlayTime
            val progress = updateListener.animatedValue as Float
            rippleShader.rawProgress = progress
            rippleShader.distortionStrength = 1 - progress
            rippleShader.time = now.toFloat()
            invalidate()
        }
        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    onAnimationEnd?.run()
                }
            }
        )
        animator.start()
    }

    /**
     * Set the color to be used for the ripple.
     *
     * The alpha value of the color will be applied to the ripple. The alpha range is [0-255].
     */
    fun setColor(color: Int, alpha: Int = RippleShader.RIPPLE_DEFAULT_ALPHA) {
        rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
    }

    /** Set the intensity of the sparkles. */
    fun setSparkleStrength(strength: Float) {
        rippleShader.sparkleStrength = strength
    }

    /** Indicates whether the ripple animation is playing. */
    fun rippleInProgress(): Boolean = animator.isRunning

    override fun onDraw(canvas: Canvas) {
        if (!canvas.isHardwareAccelerated) {
            // Drawing with the ripple shader requires hardware acceleration, so skip if it's
            // unsupported.
            return
        }
        // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
        // active effect area. Values here should be kept in sync with the animation implementation
        // in the ripple shader.
        if (rippleShape == RippleShape.CIRCLE) {
            val maskRadius = rippleShader.rippleSize.currentWidth
            canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
        } else if (rippleShape == RippleShape.ELLIPSE) {
            val maskWidth = rippleShader.rippleSize.currentWidth * 2
            val maskHeight = rippleShader.rippleSize.currentHeight * 2
            canvas.drawRect(
                /* left= */ centerX - maskWidth,
                /* top= */ centerY - maskHeight,
                /* right= */ centerX + maskWidth,
                /* bottom= */ centerY + maskHeight,
                ripplePaint
            )
        } else { // RippleShape.RoundedBox
            // No masking for the rounded box, as it has more blur which requires larger bounds.
            // Masking creates sharp bounds even when the masking is 4 times bigger.
            canvas.drawPaint(ripplePaint)
        }
    }
}
