package com.android.systemui.statusbar.notification

import android.util.FloatProperty
import android.view.View
import androidx.annotation.FloatRange
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.flags.RefactorFlag
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import kotlin.math.abs

/**
 * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
 *
 * To request a roundness value, an [SourceType] must be specified. In case more origins require
 * different roundness, for the same property, the maximum value will always be chosen.
 *
 * It also returns the current radius for all corners ([updatedRadii]).
 */
interface Roundable {
    /** Properties required for a Roundable */
    val roundableState: RoundableState

    val clipHeight: Int

    /** Current top roundness */
    @get:FloatRange(from = 0.0, to = 1.0)
    val topRoundness: Float
        get() = roundableState.topRoundness

    /** Current bottom roundness */
    @get:FloatRange(from = 0.0, to = 1.0)
    val bottomRoundness: Float
        get() = roundableState.bottomRoundness

    /** Max radius in pixel */
    val maxRadius: Float
        get() = roundableState.maxRadius

    /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
    val topCornerRadius: Float
        get() =
            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.topCornerRadius
            else topRoundness * maxRadius

    /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
    val bottomCornerRadius: Float
        get() =
            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.bottomCornerRadius
            else bottomRoundness * maxRadius

    /** Get and update the current radii */
    val updatedRadii: FloatArray
        get() =
            roundableState.radiiBuffer.also { radii ->
                updateRadii(
                    topCornerRadius = topCornerRadius,
                    bottomCornerRadius = bottomCornerRadius,
                    radii = radii,
                )
            }

    /**
     * Request the top roundness [value] for a specific [sourceType].
     *
     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
     * origins require different roundness, for the same property, the maximum value will always be
     * chosen.
     *
     * @param value a value between 0f and 1f.
     * @param animate true if it should animate to that value.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    fun requestTopRoundness(
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        sourceType: SourceType,
        animate: Boolean,
    ): Boolean {
        val roundnessMap = roundableState.topRoundnessMap
        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
        if (value == 0f) {
            // we should only take the largest value, and since the smallest value is 0f, we can
            // remove this value from the list. In the worst case, the list is empty and the
            // default value is 0f.
            roundnessMap.remove(sourceType)
        } else {
            roundnessMap[sourceType] = value
        }
        val newValue = roundnessMap.values.maxOrNull() ?: 0f

        if (lastValue != newValue) {
            val wasAnimating = roundableState.isTopAnimating()

            // Fail safe:
            // when we've been animating previously and we're now getting an update in the
            // other direction, make sure to animate it too, otherwise, the localized updating
            // may make the start larger than 1.0.
            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f

            roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
            return true
        }
        return false
    }

    /**
     * Request the top roundness [value] for a specific [sourceType]. Animate the roundness if the
     * view is shown.
     *
     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
     * origins require different roundness, for the same property, the maximum value will always be
     * chosen.
     *
     * @param value a value between 0f and 1f.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    fun requestTopRoundness(
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        sourceType: SourceType,
    ): Boolean {
        return requestTopRoundness(
            value = value,
            sourceType = sourceType,
            animate = roundableState.targetView.isShown
        )
    }

    /**
     * Request the bottom roundness [value] for a specific [sourceType].
     *
     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
     * origins require different roundness, for the same property, the maximum value will always be
     * chosen.
     *
     * @param value value between 0f and 1f.
     * @param animate true if it should animate to that value.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    fun requestBottomRoundness(
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        sourceType: SourceType,
        animate: Boolean,
    ): Boolean {
        val roundnessMap = roundableState.bottomRoundnessMap
        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
        if (value == 0f) {
            // we should only take the largest value, and since the smallest value is 0f, we can
            // remove this value from the list. In the worst case, the list is empty and the
            // default value is 0f.
            roundnessMap.remove(sourceType)
        } else {
            roundnessMap[sourceType] = value
        }
        val newValue = roundnessMap.values.maxOrNull() ?: 0f

        if (lastValue != newValue) {
            val wasAnimating = roundableState.isBottomAnimating()

            // Fail safe:
            // when we've been animating previously and we're now getting an update in the
            // other direction, make sure to animate it too, otherwise, the localized updating
            // may make the start larger than 1.0.
            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f

            roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
            return true
        }
        return false
    }

    /**
     * Request the bottom roundness [value] for a specific [sourceType]. Animate the roundness if
     * the view is shown.
     *
     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
     * origins require different roundness, for the same property, the maximum value will always be
     * chosen.
     *
     * @param value value between 0f and 1f.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    fun requestBottomRoundness(
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        sourceType: SourceType,
    ): Boolean {
        return requestBottomRoundness(
            value = value,
            sourceType = sourceType,
            animate = roundableState.targetView.isShown
        )
    }

    /**
     * Request the roundness [value] for a specific [sourceType].
     *
     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
     * more origins require different roundness, for the same property, the maximum value will
     * always be chosen.
     *
     * @param top top value between 0f and 1f.
     * @param bottom bottom value between 0f and 1f.
     * @param sourceType the source from which the request for roundness comes.
     * @param animate true if it should animate to that value.
     * @return Whether the roundness was changed.
     */
    fun requestRoundness(
        @FloatRange(from = 0.0, to = 1.0) top: Float,
        @FloatRange(from = 0.0, to = 1.0) bottom: Float,
        sourceType: SourceType,
        animate: Boolean,
    ): Boolean {
        val hasTopChanged =
            requestTopRoundness(value = top, sourceType = sourceType, animate = animate)
        val hasBottomChanged =
            requestBottomRoundness(value = bottom, sourceType = sourceType, animate = animate)
        return hasTopChanged || hasBottomChanged
    }

    /**
     * Request the roundness [value] for a specific [sourceType]. Animate the roundness if the view
     * is shown.
     *
     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
     * more origins require different roundness, for the same property, the maximum value will
     * always be chosen.
     *
     * @param top top value between 0f and 1f.
     * @param bottom bottom value between 0f and 1f.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    fun requestRoundness(
        @FloatRange(from = 0.0, to = 1.0) top: Float,
        @FloatRange(from = 0.0, to = 1.0) bottom: Float,
        sourceType: SourceType,
    ): Boolean {
        return requestRoundness(
            top = top,
            bottom = bottom,
            sourceType = sourceType,
            animate = roundableState.targetView.isShown,
        )
    }

    /**
     * Request the roundness 0f for a [SourceType].
     *
     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
     * more origins require different roundness, for the same property, the maximum value will
     * always be chosen.
     *
     * @param sourceType the source from which the request for roundness comes.
     * @param animate true if it should animate to that value.
     */
    fun requestRoundnessReset(sourceType: SourceType, animate: Boolean) {
        requestRoundness(top = 0f, bottom = 0f, sourceType = sourceType, animate = animate)
    }

    /**
     * Request the roundness 0f for a [SourceType]. Animate the roundness if the view is shown.
     *
     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
     * more origins require different roundness, for the same property, the maximum value will
     * always be chosen.
     *
     * @param sourceType the source from which the request for roundness comes.
     */
    fun requestRoundnessReset(sourceType: SourceType) {
        requestRoundnessReset(sourceType = sourceType, animate = roundableState.targetView.isShown)
    }

    /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
    fun applyRoundnessAndInvalidate() {
        roundableState.targetView.invalidate()
    }

    /** @return true if top or bottom roundness is not zero. */
    fun hasRoundedCorner(): Boolean {
        return topRoundness != 0f || bottomRoundness != 0f
    }

    /**
     * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
     * [android.graphics.Path.addRoundRect].
     *
     * This method reuses the previous [radii] for performance reasons.
     */
    fun updateRadii(
        topCornerRadius: Float,
        bottomCornerRadius: Float,
        radii: FloatArray,
    ) {
        if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")

        if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
            (0..3).forEach { radii[it] = topCornerRadius }
            (4..7).forEach { radii[it] = bottomCornerRadius }
        }
    }
}

/**
 * State object for a `Roundable` class.
 *
 * @param targetView Will handle the [AnimatableProperty]
 * @param roundable Target of the radius animation
 * @param maxRadius Max corner radius in pixels
 */
class RoundableState
@JvmOverloads
constructor(
    internal val targetView: View,
    private val roundable: Roundable,
    maxRadius: Float,
    featureFlags: FeatureFlags? = null
) {
    internal var maxRadius = maxRadius
        private set

    internal val newHeadsUpAnim = RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS, featureFlags)

    /** Animatable for top roundness */
    private val topAnimatable = topAnimatable(roundable)

    /** Animatable for bottom roundness */
    private val bottomAnimatable = bottomAnimatable(roundable)

    /** Current top roundness. Use [setTopRoundness] to update this value */
    @set:FloatRange(from = 0.0, to = 1.0)
    internal var topRoundness = 0f
        private set

    /** Current bottom roundness. Use [setBottomRoundness] to update this value */
    @set:FloatRange(from = 0.0, to = 1.0)
    internal var bottomRoundness = 0f
        private set

    internal val topCornerRadius: Float
        get() {
            val height = roundable.clipHeight
            val topRadius = topRoundness * maxRadius
            val bottomRadius = bottomRoundness * maxRadius

            if (height == 0) {
                return 0f
            } else if (topRadius + bottomRadius > height) {
                // The sum of top and bottom corner radii should be at max the clipped height
                val overShoot = topRadius + bottomRadius - height
                return topRadius - (overShoot * topRoundness / (topRoundness + bottomRoundness))
            }

            return topRadius
        }

    internal val bottomCornerRadius: Float
        get() {
            val height = roundable.clipHeight
            val topRadius = topRoundness * maxRadius
            val bottomRadius = bottomRoundness * maxRadius

            if (height == 0) {
                return 0f
            } else if (topRadius + bottomRadius > height) {
                // The sum of top and bottom corner radii should be at max the clipped height
                val overShoot = topRadius + bottomRadius - height
                return bottomRadius -
                    (overShoot * bottomRoundness / (topRoundness + bottomRoundness))
            }

            return bottomRadius
        }

    /** Last requested top roundness associated by [SourceType] */
    internal val topRoundnessMap = mutableMapOf<SourceType, Float>()

    /** Last requested bottom roundness associated by [SourceType] */
    internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()

    /** Last cached radii */
    internal val radiiBuffer = FloatArray(8)

    /** Is top roundness animation in progress? */
    internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)

    /** Is bottom roundness animation in progress? */
    internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)

    /** Set the current top roundness */
    internal fun setTopRoundness(
        value: Float,
        animated: Boolean,
    ) {
        PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
    }

    /** Set the current bottom roundness */
    internal fun setBottomRoundness(
        value: Float,
        animated: Boolean,
    ) {
        PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
    }

    fun setMaxRadius(radius: Float) {
        if (maxRadius != radius) {
            maxRadius = radius
            roundable.applyRoundnessAndInvalidate()
        }
    }

    fun debugString() = buildString {
        append("Roundable { ")
        append("top: { value: $topRoundness, requests: $topRoundnessMap}")
        append(", ")
        append("bottom: { value: $bottomRoundness, requests: $bottomRoundnessMap}")
        append("}")
    }

    companion object {
        private val DURATION: AnimationProperties =
            AnimationProperties()
                .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())

        private fun topAnimatable(roundable: Roundable): AnimatableProperty =
            AnimatableProperty.from(
                object : FloatProperty<View>("topRoundness") {
                    override fun get(view: View): Float = roundable.topRoundness

                    override fun setValue(view: View, value: Float) {
                        roundable.roundableState.topRoundness = value
                        roundable.applyRoundnessAndInvalidate()
                    }
                },
                R.id.top_roundess_animator_tag,
                R.id.top_roundess_animator_end_tag,
                R.id.top_roundess_animator_start_tag,
            )

        private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
            AnimatableProperty.from(
                object : FloatProperty<View>("bottomRoundness") {
                    override fun get(view: View): Float = roundable.bottomRoundness

                    override fun setValue(view: View, value: Float) {
                        roundable.roundableState.bottomRoundness = value
                        roundable.applyRoundnessAndInvalidate()
                    }
                },
                R.id.bottom_roundess_animator_tag,
                R.id.bottom_roundess_animator_end_tag,
                R.id.bottom_roundess_animator_start_tag,
            )
    }
}

/**
 * Interface used to define the owner of a roundness. Usually the [SourceType] is defined as a
 * private property of a class.
 */
interface SourceType {
    companion object {
        /**
         * This is the most convenient way to define a new [SourceType].
         *
         * For example:
         * ```kotlin
         *     private val SECTION = SourceType.from("Section")
         * ```
         */
        @JvmStatic
        fun from(name: String) =
            object : SourceType {
                override fun toString() = name
            }
    }
}
