package com.android.systemui.statusbar

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.PointF
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode
import android.graphics.RadialGradient
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.android.systemui.animation.Interpolators

/**
 * Provides methods to modify the various properties of a [LightRevealScrim] to reveal between 0% to
 * 100% of the view(s) underneath the scrim.
 */
interface LightRevealEffect {
    fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim)

    companion object {

        /**
         * Returns the percent that the given value is past the threshold value. For example, 0.9 is
         * 50% of the way past 0.8.
         */
        fun getPercentPastThreshold(value: Float, threshold: Float): Float {
            return (value - threshold).coerceAtLeast(0f) * (1f / (1f - threshold))
        }
    }
}

/**
 * Light reveal effect that shows light entering the phone from the bottom of the screen. The light
 * enters from the bottom-middle as a narrow oval, and moves upward, eventually widening to fill the
 * screen.
 */
object LiftReveal : LightRevealEffect {

    /** Widen the oval of light after 35%, so it will eventually fill the screen. */
    private const val WIDEN_OVAL_THRESHOLD = 0.35f

    /** After 85%, fade out the black color at the end of the gradient. */
    private const val FADE_END_COLOR_OUT_THRESHOLD = 0.85f

    /** The initial width of the light oval, in percent of scrim width. */
    private const val OVAL_INITIAL_WIDTH_PERCENT = 0.5f

    /** The initial top value of the light oval, in percent of scrim height. */
    private const val OVAL_INITIAL_TOP_PERCENT = 1.05f

    /** The initial bottom value of the light oval, in percent of scrim height. */
    private const val OVAL_INITIAL_BOTTOM_PERCENT = 1.2f

    /** Interpolator to use for the reveal amount. */
    private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE

    override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
        val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
        val ovalWidthIncreaseAmount =
                LightRevealEffect.getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)

        val initialWidthMultiplier = (1f - OVAL_INITIAL_WIDTH_PERCENT) / 2f

        with(scrim) {
            revealGradientEndColorAlpha = 1f - LightRevealEffect.getPercentPastThreshold(
                    amount, FADE_END_COLOR_OUT_THRESHOLD)
            setRevealGradientBounds(
                    scrim.width * initialWidthMultiplier +
                            -scrim.width * ovalWidthIncreaseAmount,
                    scrim.height * OVAL_INITIAL_TOP_PERCENT -
                            scrim.height * interpolatedAmount,
                    scrim.width * (1f - initialWidthMultiplier) +
                            scrim.width * ovalWidthIncreaseAmount,
                    scrim.height * OVAL_INITIAL_BOTTOM_PERCENT +
                            scrim.height * interpolatedAmount)
        }
    }
}

class CircleReveal(
    /** X-value of the circle center of the reveal. */
    val centerX: Float,
    /** Y-value of the circle center of the reveal. */
    val centerY: Float,
    /** Radius of initial state of circle reveal */
    val startRadius: Float,
    /** Radius of end state of circle reveal */
    val endRadius: Float
) : LightRevealEffect {
    override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
        val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(amount)
        val fadeAmount =
            LightRevealEffect.getPercentPastThreshold(interpolatedAmount, 0.75f)
        val radius = startRadius + ((endRadius - startRadius) * interpolatedAmount)
        scrim.revealGradientEndColorAlpha = 1f - fadeAmount
        scrim.setRevealGradientBounds(
            centerX - radius /* left */,
            centerY - radius /* top */,
            centerX + radius /* right */,
            centerY + radius /* bottom */
        )
    }
}

class PowerButtonReveal(
    /** Approximate Y-value of the center of the power button on the physical device. */
    val powerButtonY: Float
) : LightRevealEffect {

    private val OVAL_INITIAL_HEIGHT = 50f

    private val WIDTH_INCREASE_MULTIPLIER = 1.25f

    override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
        val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN_REVERSE.getInterpolation(amount)
        val fadeAmount =
                LightRevealEffect.getPercentPastThreshold(interpolatedAmount, 0.5f)

        with(scrim) {
            revealGradientEndColorAlpha = 1f - fadeAmount
            setRevealGradientBounds(
                    width -
                            width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount,
                    powerButtonY - (OVAL_INITIAL_HEIGHT / 2f) -
                            height * interpolatedAmount,
                    width * WIDTH_INCREASE_MULTIPLIER +
                            width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount,
                    powerButtonY + (OVAL_INITIAL_HEIGHT / 2f) +
                            height * interpolatedAmount)
        }
    }
}

/**
 * Scrim view that partially reveals the content underneath it using a [RadialGradient] with a
 * transparent center. The center position, size, and stops of the gradient can be manipulated to
 * reveal views below the scrim as if they are being 'lit up'.
 */
class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    /**
     * How much of the underlying views are revealed, in percent. 0 means they will be completely
     * obscured and 1 means they'll be fully visible.
     */
    var revealAmount: Float = 0f
        set(value) {
            if (field != value) {
                field = value

                revealEffect.setRevealAmountOnScrim(value, this)
                invalidate()
            }
        }

    /**
     * The [LightRevealEffect] used to manipulate the radial gradient whenever [revealAmount]
     * changes.
     */
    var revealEffect: LightRevealEffect = LiftReveal
        set(value) {
            if (field != value) {
                field = value

                revealEffect.setRevealAmountOnScrim(revealAmount, this)
                invalidate()
            }
        }

    var revealGradientCenter = PointF()
    var revealGradientWidth: Float = 0f
    var revealGradientHeight: Float = 0f

    var revealGradientEndColor: Int = Color.BLACK
        set(value) {
            if (field != value) {
                field = value
                setPaintColorFilter()
            }
        }

    var revealGradientEndColorAlpha = 0f
        set(value) {
            if (field != value) {
                field = value
                setPaintColorFilter()
            }
        }

    /**
     * Paint used to draw a transparent-to-white radial gradient. This will be scaled and translated
     * via local matrix in [onDraw] so we never need to construct a new shader.
     */
    private val gradientPaint = Paint().apply {
        shader = RadialGradient(
                0f, 0f, 1f,
                intArrayOf(Color.TRANSPARENT, Color.WHITE), floatArrayOf(0f, 1f),
                Shader.TileMode.CLAMP)

        // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same
        // window, rather than outright replacing them.
        xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
    }

    /**
     * Matrix applied to [gradientPaint]'s RadialGradient shader to move the gradient to
     * [revealGradientCenter] and set its size to [revealGradientWidth]/[revealGradientHeight],
     * without needing to construct a new shader each time those properties change.
     */
    private val shaderGradientMatrix = Matrix()

    /**
     * Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is
     * simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
     * [revealGradientHeight] for you.
     *
     * This method does not call [invalidate] - you should do so once you're done changing
     * properties.
     */
    public fun setRevealGradientBounds(left: Float, top: Float, right: Float, bottom: Float) {
        revealGradientWidth = right - left
        revealGradientHeight = bottom - top

        revealGradientCenter.x = left + (revealGradientWidth / 2f)
        revealGradientCenter.y = top + (revealGradientHeight / 2f)
    }

    override fun onDraw(canvas: Canvas?) {
        if (canvas == null || revealGradientWidth <= 0 || revealGradientHeight <= 0) {
            return
        }

        with(shaderGradientMatrix) {
            setScale(revealGradientWidth, revealGradientHeight, 0f, 0f)
            postTranslate(revealGradientCenter.x, revealGradientCenter.y)

            gradientPaint.shader.setLocalMatrix(this)
        }

        // Draw the gradient over the screen, then multiply the end color by it.
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint)
    }

    private fun setPaintColorFilter() {
        gradientPaint.colorFilter = PorterDuffColorFilter(
                Color.argb(
                        (revealGradientEndColorAlpha * 255).toInt(),
                        Color.red(revealGradientEndColor),
                        Color.green(revealGradientEndColor),
                        Color.blue(revealGradientEndColor)),
                PorterDuff.Mode.MULTIPLY)
    }
}