package com.android.systemui.statusbar.phone

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.database.ContentObserver
import android.os.Handler
import android.os.PowerManager
import android.provider.Settings
import android.view.Display
import android.view.Surface
import android.view.View
import android.view.WindowManager.fixScale
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF
import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
import com.android.systemui.DejankUtils
import com.android.app.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.StatusBarStateControllerImpl
import com.android.systemui.statusbar.notification.AnimatableProperty
import com.android.systemui.statusbar.notification.PropertyAnimator
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.app.tracing.TraceUtils
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject

/**
 * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely
 * visible, because the transition to KEYGUARD causes brief jank.
 */
private const val ANIMATE_IN_KEYGUARD_DELAY = 600L

/**
 * Duration for the light reveal portion of the animation.
 */
private const val LIGHT_REVEAL_ANIMATION_DURATION = 750L

/**
 * Controller for the unlocked screen off animation, which runs when the device is going to sleep
 * and we're unlocked.
 *
 * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents
 * and then animates in the AOD UI.
 */
@SysUISingleton
class UnlockedScreenOffAnimationController @Inject constructor(
    private val context: Context,
    private val wakefulnessLifecycle: WakefulnessLifecycle,
    private val statusBarStateControllerImpl: StatusBarStateControllerImpl,
    private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>,
    private val keyguardStateController: KeyguardStateController,
    private val dozeParameters: dagger.Lazy<DozeParameters>,
    private val globalSettings: GlobalSettings,
    private val notifShadeWindowControllerLazy: dagger.Lazy<NotificationShadeWindowController>,
    private val interactionJankMonitor: InteractionJankMonitor,
    private val powerManager: PowerManager,
    private val handler: Handler = Handler(),
) : WakefulnessLifecycle.Observer, ScreenOffAnimation {
    private lateinit var centralSurfaces: CentralSurfaces
    private lateinit var shadeViewController: ShadeViewController
    /**
     * Whether or not [initialize] has been called to provide us with the StatusBar,
     * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen
     * off animation.
     */
    private var initialized = false

    private lateinit var lightRevealScrim: LightRevealScrim

    private var animatorDurationScale = 1f
    private var shouldAnimateInKeyguard = false
    private var lightRevealAnimationPlaying = false
    private var aodUiAnimationPlaying = false

    /**
     * The result of our decision whether to play the screen off animation in
     * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to
     * sleep.
     */
    private var decidedToAnimateGoingToSleep: Boolean? = null

    private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
        duration = LIGHT_REVEAL_ANIMATION_DURATION
        interpolator = Interpolators.LINEAR
        addUpdateListener {
            if (lightRevealScrim.revealEffect !is CircleReveal) {
                lightRevealScrim.revealAmount = it.animatedValue as Float
            }
            if (lightRevealScrim.isScrimAlmostOccludes &&
                    interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)) {
                // ends the instrument when the scrim almost occludes the screen.
                // because the following janky frames might not be perceptible.
                interactionJankMonitor.end(CUJ_SCREEN_OFF)
            }
        }
        addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationCancel(animation: Animator) {
                if (lightRevealScrim.revealEffect !is CircleReveal) {
                    lightRevealScrim.revealAmount = 1f
                }
            }

            override fun onAnimationEnd(animation: Animator) {
                lightRevealAnimationPlaying = false
                interactionJankMonitor.end(CUJ_SCREEN_OFF)
            }

            override fun onAnimationStart(animation: Animator) {
                interactionJankMonitor.begin(
                        notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF)
            }
        })
    }

    // FrameCallback used to delay starting the light reveal animation until the next frame
    private val startLightRevealCallback = TraceUtils.namedRunnable("startLightReveal") {
        lightRevealAnimationPlaying = true
        lightRevealAnimator.start()
    }

    private val animatorDurationScaleObserver = object : ContentObserver(null) {
        override fun onChange(selfChange: Boolean) {
            updateAnimatorDurationScale()
        }
    }

    override fun initialize(
        centralSurfaces: CentralSurfaces,
        shadeViewController: ShadeViewController,
        lightRevealScrim: LightRevealScrim
    ) {
        this.initialized = true
        this.lightRevealScrim = lightRevealScrim
        this.centralSurfaces = centralSurfaces
        this.shadeViewController = shadeViewController

        updateAnimatorDurationScale()
        globalSettings.registerContentObserver(
                Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
                /* notify for descendants */ false,
                animatorDurationScaleObserver)
        wakefulnessLifecycle.addObserver(this)
    }

    fun updateAnimatorDurationScale() {
        animatorDurationScale = fixScale(
                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
    }

    override fun shouldDelayKeyguardShow(): Boolean =
        shouldPlayAnimation()

    override fun isKeyguardShowDelayed(): Boolean =
        isAnimationPlaying()

    /**
     * Animates in the provided keyguard view, ending in the same position that it will be in on
     * AOD.
     */
    override fun animateInKeyguard(keyguardView: View, after: Runnable) {
        shouldAnimateInKeyguard = false
        keyguardView.alpha = 0f
        keyguardView.visibility = View.VISIBLE

        val currentY = keyguardView.y

        // Move the keyguard up by 10% so we can animate it back down.
        keyguardView.y = currentY - keyguardView.height * 0.1f

        val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP

        // We animate the Y properly separately using the PropertyAnimator, as the panel
        // view also needs to update the end position.
        PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
        PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY,
                AnimationProperties().setDuration(duration.toLong()),
                true /* animate */)

        // Cancel any existing CUJs before starting the animation
        interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
        PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
        PropertyAnimator.setProperty(
            keyguardView, AnimatableProperty.ALPHA, 1f,
            AnimationProperties()
                .setDelay(0)
                .setDuration(duration.toLong())
                .setAnimationEndAction {
                    aodUiAnimationPlaying = false

                    // Lock the keyguard if it was waiting for the screen off animation to end.
                    keyguardViewMediatorLazy.get().maybeHandlePendingLock()

                    // Tell the CentralSurfaces to become keyguard for real - we waited on that
                    // since it is slow and would have caused the animation to jank.
                    centralSurfaces.updateIsKeyguard()

                    // Run the callback given to us by the KeyguardVisibilityHelper.
                    after.run()

                    // Done going to sleep, reset this flag.
                    decidedToAnimateGoingToSleep = null

                    interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
                }
                .setAnimationCancelAction {
                    // If we're cancelled, reset state flags/listeners. The end action above
                    // will not be called, which is what we want since that will finish the
                    // screen off animation and show the lockscreen, which we don't want if we
                    // were cancelled.
                    aodUiAnimationPlaying = false
                    decidedToAnimateGoingToSleep = null
                    interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
                }
                .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
            true /* animate */)
        val builder = InteractionJankMonitor.Configuration.Builder
            .withView(
                    InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
                    checkNotNull(notifShadeWindowControllerLazy.get().windowRootView)
            )
            .setTag(statusBarStateControllerImpl.getClockId())

        interactionJankMonitor.begin(builder)
    }

    override fun onStartedWakingUp() {
        // Waking up, so reset this flag.
        decidedToAnimateGoingToSleep = null

        shouldAnimateInKeyguard = false
        DejankUtils.removeCallbacks(startLightRevealCallback)
        lightRevealAnimator.cancel()
        handler.removeCallbacksAndMessages(null)
    }

    override fun onFinishedWakingUp() {
        // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other
        // observers (such as CentralSurfaces) can ask us whether we were playing the screen off
        // animation and reset accordingly.
        aodUiAnimationPlaying = false

        // If we can't control the screen off animation, we shouldn't mess with the
        // CentralSurfaces's keyguard state unnecessarily.
        if (dozeParameters.get().canControlUnlockedScreenOff()) {
            // Make sure the status bar is in the correct keyguard state, forcing it if necessary.
            // This is required if the screen off animation is cancelled, since it might be
            // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled
            // and whether 'lock instantly' is enabled. We need to force it so that the state is set
            // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
            // changed parts of the UI (such as showing AOD in the shade) without actually changing
            // the StatusBarState. This ensures that the UI definitely reflects the desired state.
            centralSurfaces.updateIsKeyguard(true /* forceStateChange */)
        }
    }

    override fun startAnimation(): Boolean {
        if (shouldPlayUnlockedScreenOffAnimation()) {
            decidedToAnimateGoingToSleep = true

            shouldAnimateInKeyguard = true

            // Start the animation on the next frame. startAnimation() is called after
            // PhoneWindowManager makes a binder call to System UI on
            // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is
            // already busy making changes to PowerManager and DisplayManager. This increases our
            // chance of missing the first frame, so to mitigate this we should start the animation
            // on the next frame.
            DejankUtils.postAfterTraversal(startLightRevealCallback)
            handler.postDelayed({
                // Only run this callback if the device is sleeping (not interactive). This callback
                // is removed in onStartedWakingUp, but since that event is asynchronously
                // dispatched, a race condition could make it possible for this callback to be run
                // as the device is waking up. That results in the AOD UI being shown while we wake
                // up, with unpredictable consequences.
                if (!powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
                        shouldAnimateInKeyguard) {
                    if (!KeyguardShadeMigrationNssl.isEnabled) {
                        // Tracking this state should no longer be relevant, as the isInteractive
                        // check covers it
                        aodUiAnimationPlaying = true
                    }

                    // Show AOD. That'll cause the KeyguardVisibilityHelper to call
                    // #animateInKeyguard.
                    shadeViewController.showAodUi()
                }
            }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())

            return true
        } else {
            decidedToAnimateGoingToSleep = false
            return false
        }
    }

    /**
     * Whether we want to play the screen off animation when the phone starts going to sleep, based
     * on the current state of the device.
     */
    fun shouldPlayUnlockedScreenOffAnimation(): Boolean {
        // If we haven't been initialized yet, we don't have a StatusBar/LightRevealScrim yet, so we
        // can't perform the animation.
        if (!initialized) {
            return false
        }

        // If the device isn't in a state where we can control unlocked screen off (no AOD enabled,
        // power save, etc.) then we shouldn't try to do so.
        if (!dozeParameters.get().canControlUnlockedScreenOff()) {
            return false
        }

        // If we explicitly already decided not to play the screen off animation, then never change
        // our mind.
        if (decidedToAnimateGoingToSleep == false) {
            return false
        }

        // If animations are disabled system-wide, don't play this one either.
        if (Settings.Global.getString(
                context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") {
            return false
        }

        // We only play the unlocked screen off animation if we are... unlocked.
        if (statusBarStateControllerImpl.state != StatusBarState.SHADE) {
            return false
        }

        // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
        // already expanded and showing notifications/QS, the animation looks really messy. For now,
        // disable it if the notification panel is expanded.
        if ((!this::centralSurfaces.isInitialized ||
                shadeViewController.isPanelExpanded) &&
                // Status bar might be expanded because we have started
                // playing the animation already
                !isAnimationPlaying()
        ) {
            return false
        }

        // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
        // portrait. If we're in another orientation, disable the screen off animation so we don't
        // animate in the keyguard AOD UI sideways or upside down.
        if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
            context.display?.rotation != Surface.ROTATION_0) {
            return false
        }

        // Otherwise, good to go.
        return true
    }

    override fun shouldDelayDisplayDozeTransition(): Boolean =
        shouldPlayUnlockedScreenOffAnimation()

    /**
     * Whether we're doing the light reveal animation or we're done with that and animating in the
     * AOD UI.
     */
    override fun isAnimationPlaying(): Boolean {
        return lightRevealAnimationPlaying || aodUiAnimationPlaying
    }

    override fun shouldAnimateInKeyguard(): Boolean =
        shouldAnimateInKeyguard

    override fun shouldHideScrimOnWakeUp(): Boolean =
        isScreenOffLightRevealAnimationPlaying()

    override fun overrideNotificationsDozeAmount(): Boolean =
        shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying()

    override fun shouldShowAodIconsWhenShade(): Boolean =
        isAnimationPlaying()

    override fun shouldAnimateAodIcons(): Boolean =
        shouldPlayUnlockedScreenOffAnimation()

    override fun shouldPlayAnimation(): Boolean =
        shouldPlayUnlockedScreenOffAnimation()

    /**
     * Whether the light reveal animation is playing. The second part of the screen off animation,
     * where AOD animates in, might still be playing if this returns false.
     */
    fun isScreenOffLightRevealAnimationPlaying(): Boolean {
        return lightRevealAnimationPlaying
    }
}
