/*
 * 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.unfold

import android.annotation.BinderThread
import android.content.ContentResolver
import android.content.Context
import android.graphics.PixelFormat
import android.hardware.devicestate.DeviceStateManager
import android.hardware.devicestate.DeviceStateManager.FoldStateListener
import android.hardware.display.DisplayManager
import android.hardware.input.InputManagerGlobal
import android.os.Handler
import android.os.Looper
import android.os.Trace
import android.view.Choreographer
import android.view.Display
import android.view.DisplayInfo
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.SurfaceSession
import android.view.WindowManager
import android.view.WindowlessWindowManager
import com.android.app.tracing.traceSection
import com.android.keyguard.logging.ScrimLogger
import com.android.systemui.Flags.unfoldAnimationBackgroundProgress
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.settings.DisplayTracker
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
import com.android.systemui.util.concurrency.ThreadFactory
import com.android.wm.shell.displayareahelper.DisplayAreaHelper
import java.util.Optional
import java.util.concurrent.Executor
import java.util.function.Consumer
import javax.inject.Inject
import javax.inject.Provider

@SysUIUnfoldScope
class UnfoldLightRevealOverlayAnimation
@Inject
constructor(
    private val context: Context,
    private val featureFlags: FeatureFlags,
    private val deviceStateManager: DeviceStateManager,
    private val contentResolver: ContentResolver,
    private val displayManager: DisplayManager,
    @UnfoldBg
    private val unfoldTransitionBgProgressProvider: Provider<UnfoldTransitionProgressProvider>,
    private val unfoldTransitionProgressProvider: Provider<UnfoldTransitionProgressProvider>,
    private val displayAreaHelper: Optional<DisplayAreaHelper>,
    @Main private val executor: Executor,
    private val threadFactory: ThreadFactory,
    @UnfoldBg private val rotationChangeProvider: RotationChangeProvider,
    @UnfoldBg private val unfoldProgressHandler: Handler,
    private val displayTracker: DisplayTracker,
    private val scrimLogger: ScrimLogger,
) {

    private val transitionListener = TransitionListener()
    private val rotationWatcher = RotationWatcher()

    private lateinit var bgHandler: Handler
    private lateinit var bgExecutor: Executor

    private lateinit var wwm: WindowlessWindowManager
    private lateinit var unfoldedDisplayInfo: DisplayInfo
    private lateinit var overlayContainer: SurfaceControl

    private var root: SurfaceControlViewHost? = null
    private var scrimView: LightRevealScrim? = null
    private var isFolded: Boolean = false
    private var isUnfoldHandled: Boolean = true
    private var overlayAddReason: AddOverlayReason? = null
    private var isTouchBlocked: Boolean = true

    private var currentRotation: Int = context.display!!.rotation

    fun init() {
        // This method will be called only on devices where this animation is enabled,
        // so normally this thread won't be created
        bgHandler = unfoldProgressHandler
        bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler)

        deviceStateManager.registerCallback(bgExecutor, FoldListener())
        if (unfoldAnimationBackgroundProgress()) {
            unfoldTransitionBgProgressProvider.get().addCallback(transitionListener)
        } else {
            unfoldTransitionProgressProvider.get().addCallback(transitionListener)
        }
        rotationChangeProvider.addCallback(rotationWatcher)

        val containerBuilder =
            SurfaceControl.Builder(SurfaceSession())
                .setContainerLayer()
                .setName("unfold-overlay-container")

        displayAreaHelper.get().attachToRootDisplayArea(
            displayTracker.defaultDisplayId,
            containerBuilder
        ) { builder ->
            executor.execute {
                overlayContainer = builder.build()

                SurfaceControl.Transaction()
                    .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX)
                    .show(overlayContainer)
                    .apply()

                wwm =
                    WindowlessWindowManager(context.resources.configuration, overlayContainer, null)
            }
        }

        // Get unfolded display size immediately as 'current display info' might be
        // not up-to-date during unfolding
        unfoldedDisplayInfo = getUnfoldedDisplayInfo()
    }

    /**
     * Called when screen starts turning on, the contents of the screen might not be visible yet.
     * This method reports back that the overlay is ready in [onOverlayReady] callback.
     *
     * @param onOverlayReady callback when the overlay is drawn and visible on the screen
     * @see [com.android.systemui.keyguard.KeyguardViewMediator]
     */
    @BinderThread
    fun onScreenTurningOn(onOverlayReady: Runnable) {
        executeInBackground {
            Trace.beginSection("$TAG#onScreenTurningOn")
            try {
                // Add the view only if we are unfolding and this is the first screen on
                if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) {
                    addOverlay(onOverlayReady, reason = UNFOLD)
                    isUnfoldHandled = true
                } else {
                    // No unfold transition, immediately report that overlay is ready
                    ensureOverlayRemoved()
                    onOverlayReady.run()
                }
            } finally {
                Trace.endSection()
            }
        }
    }

    private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) {
        if (!::wwm.isInitialized) {
            // Surface overlay is not created yet on the first SysUI launch
            onOverlayReady?.run()
            return
        }

        ensureInBackground()
        ensureOverlayRemoved()

        overlayAddReason = reason

        val newRoot =
            SurfaceControlViewHost(
                context,
                context.display,
                wwm,
                "UnfoldLightRevealOverlayAnimation"
            )
        val params = getLayoutParams()
        val newView =
            LightRevealScrim(
                    context,
                    attrs = null,
                    initialWidth = params.width,
                    initialHeight = params.height
                )
                .apply {
                    revealEffect = createLightRevealEffect()
                    revealAmount = calculateRevealAmount()
                    scrimLogger = this@UnfoldLightRevealOverlayAnimation.scrimLogger
                }

        newRoot.setView(newView, params)

        if (onOverlayReady != null) {
            Trace.beginAsyncSection("$TAG#relayout", 0)

            newRoot.relayout(params) { transaction ->
                val vsyncId = Choreographer.getSfInstance().vsyncId

                // Apply the transaction that contains the first frame of the overlay and apply
                // another empty transaction with 'vsyncId + 1' to make sure that it is actually
                // displayed on the screen. The second transaction is necessary to remove the screen
                // blocker (turn on the brightness) only when the content is actually visible as it
                // might be presented only in the next frame.
                // See b/197538198
                transaction.setFrameTimelineVsync(vsyncId).apply()

                transaction
                    .setFrameTimelineVsync(vsyncId + 1)
                    .addTransactionCommittedListener(bgExecutor) {
                        Trace.endAsyncSection("$TAG#relayout", 0)
                        onOverlayReady.run()
                    }
                    .apply()
            }
        }

        scrimView = newView
        root = newRoot
    }

    private fun calculateRevealAmount(animationProgress: Float? = null): Float {
        val overlayAddReason = overlayAddReason ?: UNFOLD

        if (animationProgress == null) {
            // Animation progress is unknown, calculate the initial value based on the overlay
            // add reason
            return when (overlayAddReason) {
                FOLD -> TRANSPARENT
                UNFOLD -> BLACK
            }
        }

        val showVignetteWhenFolding =
            featureFlags.isEnabled(Flags.ENABLE_DARK_VIGNETTE_WHEN_FOLDING)

        return if (!showVignetteWhenFolding && overlayAddReason == FOLD) {
            // Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off
            // and we are folding the device. We still add the overlay to block touches
            // while the animation is running but the overlay is transparent.
            TRANSPARENT
        } else {
            animationProgress
        }
    }

    private fun getLayoutParams(): WindowManager.LayoutParams {
        val params: WindowManager.LayoutParams = WindowManager.LayoutParams()

        val rotation = currentRotation
        val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180

        params.height =
            if (isNatural) unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth
        params.width =
            if (isNatural) unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight

        params.format = PixelFormat.TRANSLUCENT
        params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
        params.title = "Unfold Light Reveal Animation"
        params.layoutInDisplayCutoutMode =
            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
        params.fitInsetsTypes = 0

        val touchFlags =
            if (isTouchBlocked) {
                // Touchable by default, so it will block the touches
                0
            } else {
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
            }
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchFlags
        params.setTrustedOverlay()

        val packageName: String = context.opPackageName
        params.packageName = packageName

        return params
    }

    private fun updateTouchBlockIfNeeded(progress: Float) {
        // When unfolding unblock touches a bit earlier than the animation end as the
        // interpolation has a long tail of very slight movement at the end which should not
        // affect much the usage of the device
        val shouldBlockTouches =
            if (overlayAddReason == UNFOLD) {
                progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS
            } else {
                true
            }

        if (isTouchBlocked != shouldBlockTouches) {
            isTouchBlocked = shouldBlockTouches

            traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) }
        }
    }

    private fun createLightRevealEffect(): LightRevealEffect {
        val isVerticalFold =
            currentRotation == Surface.ROTATION_0 || currentRotation == Surface.ROTATION_180
        return LinearLightRevealEffect(isVertical = isVerticalFold)
    }

    private fun ensureOverlayRemoved() {
        ensureInBackground()
        traceSection("ensureOverlayRemoved") {
            root?.release()
            root = null
            scrimView = null
        }
    }

    private fun getUnfoldedDisplayInfo(): DisplayInfo =
        displayManager
            .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
            .asSequence()
            .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
            .filter { it.type == Display.TYPE_INTERNAL }
            .maxByOrNull { it.naturalWidth }!!

    private inner class TransitionListener : TransitionProgressListener {

        override fun onTransitionProgress(progress: Float) {
            executeInBackground {
                scrimView?.revealAmount = calculateRevealAmount(progress)
                updateTouchBlockIfNeeded(progress)
            }
        }

        override fun onTransitionFinished() {
            executeInBackground { ensureOverlayRemoved() }
        }

        override fun onTransitionStarted() {
            // Add view for folding case (when unfolding the view is added earlier)
            if (scrimView == null) {
                executeInBackground { addOverlay(reason = FOLD) }
            }
            // Disable input dispatching during transition.
            InputManagerGlobal.getInstance().cancelCurrentTouch()
        }
    }

    private inner class RotationWatcher : RotationChangeProvider.RotationListener {
        override fun onRotationChanged(newRotation: Int) {
            executeInBackground {
                traceSection("$TAG#onRotationChanged") {
                    if (currentRotation != newRotation) {
                        currentRotation = newRotation
                        scrimView?.revealEffect = createLightRevealEffect()
                        root?.relayout(getLayoutParams())
                    }
                }
            }
        }
    }

    private fun executeInBackground(f: () -> Unit) {
        // This is needed to allow progresses to be received both from the main thread (that will
        // schedule a runnable on the bg thread), and from the bg thread directly (no reposting).
        if (bgHandler.looper.isCurrentThread) {
            f()
        } else {
            bgHandler.post(f)
        }
    }

    private fun ensureInBackground() {
        check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" }
    }

    private inner class FoldListener :
        FoldStateListener(
            context,
            Consumer { isFolded ->
                if (isFolded) {
                    ensureOverlayRemoved()
                    isUnfoldHandled = false
                }
                this.isFolded = isFolded
            }
        )

    private enum class AddOverlayReason {
        FOLD,
        UNFOLD
    }

    private companion object {
        const val TAG = "UnfoldLightRevealOverlayAnimation"
        const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE

        // Put the unfold overlay below the rotation animation screenshot to hide the moment
        // when it is rotated but the rotation of the other windows hasn't happen yet
        const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1

        // constants for revealAmount.
        const val TRANSPARENT = 1f
        const val BLACK = 0f

        private const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
    }
}
