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

import android.annotation.SuppressLint
import android.annotation.UiThread
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.Rect
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
import android.hardware.biometrics.BiometricRequestConstants.RequestReason
import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
import android.os.Build
import android.os.RemoteException
import android.provider.Settings
import android.util.Log
import android.util.RotationUtils
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.Surface
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import com.android.systemui.biometrics.ui.binder.UdfpsTouchOverlayBinder
import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay
import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayViewModel
import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.res.R
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import dagger.Lazy
import kotlinx.coroutines.ExperimentalCoroutinesApi

private const val TAG = "UdfpsControllerOverlay"

@VisibleForTesting
const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"

/**
 * Keeps track of the overlay state and UI resources associated with a single FingerprintService
 * request. This state can persist across configuration changes via the [show] and [hide]
 * methods.
 */
@ExperimentalCoroutinesApi
@UiThread
class UdfpsControllerOverlay @JvmOverloads constructor(
    private val context: Context,
    private val inflater: LayoutInflater,
    private val windowManager: WindowManager,
    private val accessibilityManager: AccessibilityManager,
    private val statusBarStateController: StatusBarStateController,
    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
    private val dialogManager: SystemUIDialogManager,
    private val dumpManager: DumpManager,
    private val transitionController: LockscreenShadeTransitionController,
    private val configurationController: ConfigurationController,
    private val keyguardStateController: KeyguardStateController,
    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
    private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
    val requestId: Long,
    @RequestReason val requestReason: Int,
    private val controllerCallback: IUdfpsOverlayControllerCallback,
    private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
    private val activityLaunchAnimator: ActivityLaunchAnimator,
    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
    private val alternateBouncerInteractor: AlternateBouncerInteractor,
    private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
    private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
    private val transitionInteractor: KeyguardTransitionInteractor,
    private val selectedUserInteractor: SelectedUserInteractor,
    private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
    private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
) {
    private var overlayViewLegacy: UdfpsView? = null
        private set
    private var overlayTouchView: UdfpsTouchOverlay? = null

    /**
     * Get the current UDFPS overlay touch view which is a different View depending on whether
     * the DeviceEntryUdfpsRefactor flag is enabled or not.
     * @return The view, when [isShowing], else null
     */
    fun getTouchOverlay(): View? {
        return if (DeviceEntryUdfpsRefactor.isEnabled) {
            overlayTouchView
        } else {
            overlayViewLegacy
        }
    }

    private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
    private var sensorBounds: Rect = Rect()

    private var overlayTouchListener: TouchExplorationStateChangeListener? = null

    private val coreLayoutParams = WindowManager.LayoutParams(
        WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
        0 /* flags set in computeLayoutParams() */,
        PixelFormat.TRANSLUCENT
    ).apply {
        title = TAG
        fitInsetsTypes = 0
        gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
        layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
        flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
        privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
        // Avoid announcing window title.
        accessibilityTitle = " "
        inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
    }

    /** If the overlay is currently showing. */
    val isShowing: Boolean
        get() = getTouchOverlay() != null

    /** Opposite of [isShowing]. */
    val isHiding: Boolean
        get() = getTouchOverlay() == null

    /** The animation controller if the overlay [isShowing]. */
    val animationViewController: UdfpsAnimationViewController<*>?
        get() = overlayViewLegacy?.animationViewController

    private var touchExplorationEnabled = false

    private fun shouldRemoveEnrollmentUi(): Boolean {
        if (isDebuggable) {
            return Settings.Global.getInt(
                context.contentResolver,
                SETTING_REMOVE_ENROLLMENT_UI,
                0 /* def */
            ) != 0
        }
        return false
    }

    /** Show the overlay or return false and do nothing if it is already showing. */
    @SuppressLint("ClickableViewAccessibility")
    fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
        if (getTouchOverlay() == null) {
            overlayParams = params
            sensorBounds = Rect(params.sensorBounds)
            try {
                if (DeviceEntryUdfpsRefactor.isEnabled) {
                    overlayTouchView = (inflater.inflate(
                            R.layout.udfps_touch_overlay, null, false
                    ) as UdfpsTouchOverlay).apply {
                        // This view overlaps the sensor area
                        // prevent it from being selectable during a11y
                        if (requestReason.isImportantForAccessibility()) {
                            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
                        }
                        windowManager.addView(this, coreLayoutParams.updateDimensions(null))
                        when (requestReason) {
                            REASON_AUTH_KEYGUARD ->
                                UdfpsTouchOverlayBinder.bind(
                                    view = this,
                                    viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(),
                                )
                            else ->
                                UdfpsTouchOverlayBinder.bind(
                                    view = this,
                                    viewModel = defaultUdfpsTouchOverlayViewModel.get(),
                                )
                        }
                    }
                } else {
                    overlayViewLegacy = (inflater.inflate(
                            R.layout.udfps_view, null, false
                    ) as UdfpsView).apply {
                        overlayParams = params
                        setUdfpsDisplayModeProvider(udfpsDisplayModeProvider)
                        val animation = inflateUdfpsAnimation(this, controller)
                        if (animation != null) {
                            animation.init()
                            animationViewController = animation
                        }
                        // This view overlaps the sensor area
                        // prevent it from being selectable during a11y
                        if (requestReason.isImportantForAccessibility()) {
                            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
                        }

                        windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
                        sensorRect = sensorBounds
                    }
                }
                getTouchOverlay()?.apply {
                    touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
                    overlayTouchListener = TouchExplorationStateChangeListener {
                        if (accessibilityManager.isTouchExplorationEnabled) {
                            setOnHoverListener { v, event -> onTouch(v, event, true) }
                            setOnTouchListener(null)
                            touchExplorationEnabled = true
                        } else {
                            setOnHoverListener(null)
                            setOnTouchListener { v, event -> onTouch(v, event, true) }
                            touchExplorationEnabled = false
                        }
                    }
                    accessibilityManager.addTouchExplorationStateChangeListener(
                            overlayTouchListener!!
                    )
                    overlayTouchListener?.onTouchExplorationStateChanged(true)
                }
            } catch (e: RuntimeException) {
                Log.e(TAG, "showUdfpsOverlay | failed to add window", e)
            }
            return true
        }

        Log.v(TAG, "showUdfpsOverlay | the overlay is already showing")
        return false
    }

    fun inflateUdfpsAnimation(
        view: UdfpsView,
        controller: UdfpsController
    ): UdfpsAnimationViewController<*>? {
        DeviceEntryUdfpsRefactor.assertInLegacyMode()

        val isEnrollment = when (requestReason) {
            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
            else -> false
        }

        val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
            REASON_AUTH_OTHER
        } else {
            requestReason
        }

        return when (filteredRequestReason) {
            REASON_ENROLL_FIND_SENSOR,
            REASON_ENROLL_ENROLLING -> {
                // Enroll udfps UI is handled by settings, so use empty view here
                UdfpsFpmEmptyViewController(
                    view.addUdfpsView(R.layout.udfps_fpm_empty_view){
                        updateAccessibilityViewLocation(sensorBounds)
                    },
                    statusBarStateController,
                    primaryBouncerInteractor,
                    dialogManager,
                    dumpManager
                )
            }
            REASON_AUTH_KEYGUARD -> {
                UdfpsKeyguardViewControllerLegacy(
                    view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) {
                        updateSensorLocation(sensorBounds)
                    },
                    statusBarStateController,
                    statusBarKeyguardViewManager,
                    keyguardUpdateMonitor,
                    dumpManager,
                    transitionController,
                    configurationController,
                    keyguardStateController,
                    unlockedScreenOffAnimationController,
                    dialogManager,
                    controller,
                    activityLaunchAnimator,
                    primaryBouncerInteractor,
                    alternateBouncerInteractor,
                    udfpsKeyguardAccessibilityDelegate,
                    selectedUserInteractor,
                    transitionInteractor,
                )
            }
            REASON_AUTH_BP -> {
                // note: empty controller, currently shows no visual affordance
                UdfpsBpViewController(
                    view.addUdfpsView(R.layout.udfps_bp_view),
                    statusBarStateController,
                    primaryBouncerInteractor,
                    dialogManager,
                    dumpManager
                )
            }
            REASON_AUTH_OTHER,
            REASON_AUTH_SETTINGS -> {
                UdfpsFpmEmptyViewController(
                    view.addUdfpsView(R.layout.udfps_fpm_empty_view),
                    statusBarStateController,
                    primaryBouncerInteractor,
                    dialogManager,
                    dumpManager
                )
            }
            else -> {
                Log.e(TAG, "Animation for reason $requestReason not supported yet")
                null
            }
        }
    }

    /** Hide the overlay or return false and do nothing if it is already hidden. */
    fun hide(): Boolean {
        val wasShowing = isShowing

        overlayViewLegacy?.apply {
            if (isDisplayConfigured) {
                unconfigureDisplay()
            }
            animationViewController = null
        }
        if (DeviceEntryUdfpsRefactor.isEnabled) {
            udfpsDisplayModeProvider.disable(null)
        }
        getTouchOverlay()?.apply {
            windowManager.removeView(this)
            setOnTouchListener(null)
            setOnHoverListener(null)
            overlayTouchListener?.let {
                accessibilityManager.removeTouchExplorationStateChangeListener(it)
            }
        }

        overlayViewLegacy = null
        overlayTouchView = null
        overlayTouchListener = null

        return wasShowing
    }

    /** Cancel this request. */
    fun cancel() {
        try {
            controllerCallback.onUserCanceled()
        } catch (e: RemoteException) {
            Log.e(TAG, "Remote exception", e)
        }
    }

    /** Checks if the id is relevant for this overlay. */
    fun matchesRequestId(id: Long): Boolean = requestId == -1L || requestId == id

    private fun WindowManager.LayoutParams.updateDimensions(
        animation: UdfpsAnimationViewController<*>?
    ): WindowManager.LayoutParams {
        val paddingX = animation?.paddingX ?: 0
        val paddingY = animation?.paddingY ?: 0

        val isEnrollment = when (requestReason) {
            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
            else -> false
        }

        // Use expanded overlay unless touchExploration enabled
        var rotatedBounds =
            if (accessibilityManager.isTouchExplorationEnabled && isEnrollment) {
                Rect(overlayParams.sensorBounds)
            } else {
                Rect(
                    0,
                    0,
                    overlayParams.naturalDisplayWidth,
                    overlayParams.naturalDisplayHeight
                )
            }

        val rot = overlayParams.rotation
        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
            if (!shouldRotate(animation)) {
                Log.v(
                    TAG, "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) +
                        " animation=$animation" +
                        " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" +
                        " isOccluded=${keyguardStateController.isOccluded}"
                )
            } else {
                Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot))
                RotationUtils.rotateBounds(
                    rotatedBounds,
                    overlayParams.naturalDisplayWidth,
                    overlayParams.naturalDisplayHeight,
                    rot
                )

                RotationUtils.rotateBounds(
                    sensorBounds,
                    overlayParams.naturalDisplayWidth,
                    overlayParams.naturalDisplayHeight,
                    rot
                )
            }
        }

        x = rotatedBounds.left - paddingX
        y = rotatedBounds.top - paddingY
        height = rotatedBounds.height() + 2 * paddingX
        width = rotatedBounds.width() + 2 * paddingY

        return this
    }

    private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean {
        val keyguardNotShowing =
            if (DeviceEntryUdfpsRefactor.isEnabled) {
                !keyguardStateController.isShowing
            } else {
                animation !is UdfpsKeyguardViewControllerAdapter
            }

        if (keyguardNotShowing) {
            // always rotate view if we're not on the keyguard
            return true
        }

        // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded
        return !(keyguardUpdateMonitor.isGoingToSleep || !keyguardStateController.isOccluded)
    }

    private inline fun <reified T : View> UdfpsView.addUdfpsView(
        @LayoutRes id: Int,
        init: T.() -> Unit = {}
    ): T {
        val subView = inflater.inflate(id, null) as T
        addView(subView)
        subView.init()
        return subView
    }
}

@RequestReason
private fun Int.isImportantForAccessibility() =
    this == REASON_ENROLL_FIND_SENSOR ||
            this == REASON_ENROLL_ENROLLING ||
            this == REASON_AUTH_BP
