package tk.zwander.common.util

import android.app.KeyguardManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.SparseArray
import android.view.Display
import android.view.WindowInsets
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.util.forEach
import dev.zwander.lswinterconnect.LogUtils
import dev.zwander.lswinterconnect.peekLogUtils
import kotlinx.atomicfu.AtomicBoolean
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import tk.zwander.common.activities.DismissOrUnlockActivity
import tk.zwander.common.data.window.WindowInfo
import tk.zwander.common.data.window.WindowRootPair
import tk.zwander.lockscreenwidgets.R
import tk.zwander.lockscreenwidgets.appwidget.IDListProvider
import tk.zwander.lockscreenwidgets.services.Accessibility
import tk.zwander.lockscreenwidgets.util.FrameSpecificPreferences
import tk.zwander.lockscreenwidgets.util.MainWidgetFrameDelegate
import tk.zwander.widgetdrawer.util.DrawerDelegate
import java.util.concurrent.ConcurrentLinkedQueue

object AccessibilityUtils {
    data class NodeState(
        val onMainLockscreen: AtomicBoolean = atomic(false),
        val showingNotificationsPanel: AtomicBoolean = atomic(false),
        val showingSecurityInput: AtomicBoolean = atomic(false),
        val hasMoreButton: AtomicBoolean = atomic(false),
        val hideForPresentIds: AtomicBoolean = atomic(false),
        val hideForNonPresentIds: AtomicBoolean = atomic(false),
        val hasClearAllButton: AtomicBoolean = atomic(false),
        val hasSettingsContainerButton: AtomicBoolean = atomic(false),
        val onFaceWidgets: AtomicBoolean = atomic(false),
        val hasNotificationsShowing: AtomicBoolean = atomic(false),
        val showingPowerMenu: AtomicBoolean = atomic(false),
        val showingEdgePanel: AtomicBoolean = atomic(false),
    )

    data object IDMaps {
        val mainLockScreenIds = unitMapOf(
            "com.android.systemui:id/notification_panel",
            "com.android.systemui:id/left_button",
            "com.android.systemui:id/camera_button",
            "com.android.systemui:id/keyguard_indication_text_bottom",
            "com.android.systemui:id/keyguard_carrier_text",
            "com.android.systemui:id/keyguard_owner_info",
            "com.android.systemui:id/footer_views",
        )

        val securityInputIds = unitMapOf(
            "com.android.systemui:id/keyguard_host_view",
            "com.android.systemui:id/keyguard_security_container",
            "element:BouncerContent",
        )

        val notificationsPanelIds = unitMapOf(
            "com.android.systemui:id/quick_settings_panel",
            "com.android.systemui:id/settings_button",
            "com.android.systemui:id/tile_label",
            "com.android.systemui:id/header_label",
            "com.android.systemui:id/split_shade_status_bar",
            "com.android.systemui:id/notification_shade_shortcut",
            "com.android.systemui:id/qs_footer".takeIf {
                Build.VERSION.SDK_INT <= Build.VERSION_CODES.P
            },
            "com.android.systemui:id/quick_qs_panel".takeIf {
                Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2 &&
                        Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA
            },
            // HyperOS Control Center
            "miui.systemui.plugin:id/main_panel_container",
        )

        val notificationIds = unitMapOf(
            "com.android.systemui:id/notification_guts",
            "com.android.systemui:id/expandableNotificationRow",
            "con.android.systemui:id/notificationShelf",
        )

        val moreButtonIds = unitMapOf(
            "com.android.systemui:id/more_button",
            "com.android.systemui:id/edit_button",
        )

        val settingsContainerButtonIds = unitMapOf(
            "com.android.systemui:id/settings_button_container",
        )

        val clearAllButtonIds = unitMapOf(
            "com.android.systemui:id/clear_all",
        )

        val powerMenuIds = unitMapOf(
            "com.android.systemui:id/sec_global_actions_icon",
        )

        val edgePanelIds = unitMapOf(
            "com.sec.android.daemonapp:id/edge_panel",
        )

        val sysUiPackageNames = unitMapOf(
            "com.android.systemui",
            "miui.systemui.plugin",
            "com.sec.android.app.launcher",
        )
    }

    private fun processNode(
        nodeState: NodeState,
        node: AccessibilityNodeInfo,
        presentIds: Map<String, Unit>,
        nonPresentIds: Map<String, Unit>,
        isPixelUI: Boolean,
    ) {
        if (!nodeState.onFaceWidgets.value) {
            if (node.hasWildcardId("com.samsung.android.app.aodservice:id/facewidget_") &&
                !node.hasVisibleIds(
                    unitMapOf("com.samsung.android.app.aodservice:id/facewidget_clock_container"),
                )) {
                nodeState.onFaceWidgets.value = true
            }
        }

        //If the user has enabled the option to hide the frame on security (pin, pattern, password)
        //input, we need to check if they're on the main lock screen. This is more reliable than
        //checking for the existence of a security input, since there are a lot of different possible
        //IDs, and OEMs change them. "notification_panel" and "left_button" are largely unchanged,
        //although this method isn't perfect.
        if (!nodeState.onMainLockscreen.value) {
            if (node.hasVisibleIds(IDMaps.mainLockScreenIds)) {
                nodeState.onMainLockscreen.value = true
            }
        }

        if (!nodeState.showingNotificationsPanel.value) {
            if (node.hasVisibleIds(IDMaps.notificationsPanelIds)) {
                //Used for "Hide When Notification Shade Shown" so we know when it's actually expanded.
                //Some devices don't even have left shortcuts, so also check for keyguard_indication_area.
                //Just like the showingSecurityInput check, this is probably unreliable for some devices.
                nodeState.showingNotificationsPanel.value = true
            }
        }

        if (!nodeState.showingSecurityInput.value) {
            if (node.hasVisibleIds(IDMaps.securityInputIds)) {
                nodeState.showingSecurityInput.value = true
            }
        }

        if (!nodeState.hasNotificationsShowing.value) {
            if (node.hasVisibleIds(IDMaps.notificationIds)) {
                nodeState.hasNotificationsShowing.value = true
            }
        }

        //If the option to show when the NC is fully expanded is enabled,
        //check if the frame can actually show. This checks to the "more_button"
        //ID, which is unique to One UI (it's the three-dot button), and is only
        //visible when the NC is fully expanded.
        if (!nodeState.hasMoreButton.value) {
            if (node.hasVisibleIds(IDMaps.moreButtonIds)) {
                nodeState.hasMoreButton.value = true
            }
        }

        if (!nodeState.hasSettingsContainerButton.value) {
            if (node.hasVisibleIds(IDMaps.settingsContainerButtonIds)) {
                nodeState.hasSettingsContainerButton.value = isPixelUI
            }
        }

        if (!nodeState.hasClearAllButton.value) {
            if (node.hasVisibleIds(IDMaps.clearAllButtonIds)) {
                nodeState.hasClearAllButton.value = true
            }
        }

        //Check to see if any of the user-specified IDs are present.
        //If any are, the frame will be hidden.
        if (!nodeState.hideForPresentIds.value && presentIds.isNotEmpty()) {
            if (node.hasVisibleIds(presentIds)) {
                nodeState.hideForPresentIds.value = true
            }
        }

        //Check to see if any of the user-specified IDs aren't present
        //(or are present but not visible). If any aren't, the frame will be hidden.
        if (!nodeState.hideForNonPresentIds.value && nonPresentIds.isNotEmpty()) {
            if (!node.hasVisibleIds(nonPresentIds)) {
                nodeState.hideForNonPresentIds.value = true
            }
        }

        if (!nodeState.showingPowerMenu.value) {
            if (node.hasVisibleIds(IDMaps.powerMenuIds)) {
                nodeState.showingPowerMenu.value = true
            }
        }

        if (!nodeState.showingEdgePanel.value) {
            if (
                node.hasVisibleIds(IDMaps.edgePanelIds)
            ) {
                nodeState.showingEdgePanel.value = true
            }
        }
    }

    /**
     * Find the [AccessibilityWindowInfo] and [AccessibilityNodeInfo] objects corresponding to the System UI windows.
     * Find the [AccessibilityWindowInfo] and [AccessibilityNodeInfo] corresponding to the topmost app window.
     * Find the [AccessibilityWindowInfo] and [AccessibilityNodeInfo] corresponding to the topmost non-System UI window.
     */
    private suspend fun Context.processWindows(
        windows: List<AccessibilityWindowInfo>,
    ): WindowInfo {
        val nodeState = NodeState()

        val systemUiWindows = ConcurrentLinkedQueue<WindowRootPair>()
        val sysUiWindowViewIds = ConcurrentLinkedQueue<String>()
        val sysUiWindowNodes = ConcurrentLinkedQueue<AccessibilityNodeInfo>()
        val sysUiWindowAwaits = ConcurrentLinkedQueue<Deferred<*>>()

        var topAppWindowIndex: Int = -1
        var topNonSysUiWindowIndex: Int = -1

        var topAppWindowPackageName: String? = null

        var hasScreenOffMemoWindow = false
        var hasFaceWidgetsWindow = false
        var hasEdgePanelWindow = false
        var hasHideForPresentApp = false

        var minSysUiWindowIndex = -1

        val presentIds = prefManager.presentIds.associateWith {}
        val nonPresentIds = prefManager.nonPresentIds.associateWith {}
        val isPixelUI = isPixelUI

        val processed = windows.mapIndexedParallel { index, rawWindow ->
            val safeRoot = try {
                rawWindow.root
            } catch (e: Exception) {
                logUtils.debugLog("Error getting window root", e)
                null
            }
            val isSysUi = safeRoot != null &&
                    IDMaps.sysUiPackageNames[safeRoot.packageName] != null
            val window = WindowRootPair(rawWindow, safeRoot, index)

            if (isSysUi) {
                systemUiWindows.add(window)

                if (minSysUiWindowIndex == -1) {
                    minSysUiWindowIndex = index
                }

                addAllNodesToList(
                    safeRoot,
                    sysUiWindowNodes,
                    sysUiWindowViewIds,
                    sysUiWindowAwaits,
                ) { node ->
                    launch(Dispatchers.IO) {
                        processNode(
                            nodeState = nodeState,
                            node = node,
                            presentIds = presentIds,
                            nonPresentIds = nonPresentIds,
                            isPixelUI = isPixelUI,
                        )
                    }
                }
            }

            if ((topAppWindowIndex == -1 || topAppWindowIndex > index) &&
                (window.window.type == AccessibilityWindowInfo.TYPE_APPLICATION || (
                        // This is a workaround for the Google Assistant popup on Pixel devices.
                        // It reports a window type of "-1" since the popup's real type is TYPE_VOICE_INTERACTION,
                        // but AccessibilityWindowManager#getTypeForWindowManagerWindowType() doesn't filter
                        // for this type and returns "-1" or "unknown".
                        // The package name check is because Samsung's One Handed Mode+ gesture window also reports an unknown type.
                        safeRoot?.packageName == "com.google.android.googlequicksearchbox" && window.window.type == -1))
            ) {
                logUtils.debugLog("Found app window $window", null)
                topAppWindowIndex = index
                topAppWindowPackageName = safeRoot?.packageName?.toString()
            }

            if (
                (topNonSysUiWindowIndex == -1 || topNonSysUiWindowIndex > index) &&
                window.window.type != AccessibilityWindowInfo.TYPE_APPLICATION &&
                window.window.type != AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY &&
                !isSysUi
            ) {
                topNonSysUiWindowIndex = index
            }

            if (
                safeRoot?.packageName == "com.samsung.android.app.cocktailbarservice" &&
                safeRoot.isVisibleToUser
            ) {
                hasEdgePanelWindow = true
            }

            if (safeRoot?.packageName == "com.samsung.android.app.notes" &&
                safeRoot.isVisibleToUser
            ) {
                hasScreenOffMemoWindow = true
            }

            if (safeRoot?.packageName == "com.samsung.android.app.aodservice" &&
                safeRoot.isVisibleToUser
            ) {
                hasFaceWidgetsWindow = true
            }

            if (prefManager.hideFrameOnApps.contains(safeRoot?.packageName) &&
                safeRoot?.isVisibleToUser == true
            ) {
                hasHideForPresentApp = true
            }

            window
        }

        sysUiWindowAwaits.awaitAll()

        return WindowInfo(
            windows = processed,
            topAppWindowIndex = topAppWindowIndex,
            topNonSysUiWindowIndex = topNonSysUiWindowIndex,
            hasScreenOffMemoWindow = hasScreenOffMemoWindow,
            hasFaceWidgetsWindow = hasFaceWidgetsWindow,
            minSysUiWindowIndex = minSysUiWindowIndex,
            sysUiWindowNodes = sysUiWindowNodes,
            sysUiWindowViewIds = sysUiWindowViewIds,
            hasEdgePanelWindow = hasEdgePanelWindow,
            topAppWindowPackageName = topAppWindowPackageName,
            hasHideForPresentApp = hasHideForPresentApp,
            nodeState = nodeState,
        )
    }

    /**
     * Recursively add all [AccessibilityNodeInfo]s contained in a parent to a list.
     * We use this instead of [AccessibilityNodeInfo.findAccessibilityNodeInfosByViewId]
     * for performance and reliability reasons.
     * @param parentNode the root [AccessibilityNodeInfo] whose children we want to add to
     * a list. List will include this node as well.
     * @param list the list that will contain the child nodes.
     * @param ids the list of all visible IDs
     * @param awaits a list of all running Jobs here. The caller should use awaitAll().
     *
     * TODO: This has to recursively traverse the View tree it's given. [AccessibilityNodeInfo.getChild]
     * TODO: can be pretty slow. All adjacent nodes of the tree are processed concurrently, but their
     * TODO: children have to be processed linearly. We need a flattened list of [AccessibilityNodeInfo] objects
     * TODO: to properly handle frame visibility, but there may be a better way to do that, since this can take
     * TODO: up to a second to finish executing.
     */
    private fun CoroutineScope.addAllNodesToList(
        parentNode: AccessibilityNodeInfo,
        list: ConcurrentLinkedQueue<AccessibilityNodeInfo>,
        ids: ConcurrentLinkedQueue<String>,
        awaits: ConcurrentLinkedQueue<Deferred<*>>,
        nodeAddedCallback: (CoroutineScope.(AccessibilityNodeInfo) -> Unit),
    ) {
        list.add(parentNode)
        nodeAddedCallback(parentNode)

        if (parentNode.isVisibleToUser && parentNode.viewIdResourceName != null) {
            ids.add(parentNode.viewIdResourceName)
        }

        for (i in 0 until parentNode.childCount) {
            awaits.add(async {
                val child = try {
                    parentNode.getChild(i)
                } catch (e: SecurityException) {
                    //Sometimes a SecurityException gets thrown here (on Huawei devices)
                    //so just return null if it happens
                    peekLogUtils?.debugLog("Error getting child node", e)
                    null
                } catch (e: NullPointerException) {
                    //Sometimes a NullPointerException is thrown here with this error:
                    //"Attempt to read from field 'com.android.server.appwidget.AppWidgetServiceImpl$ProviderId
                    //com.android.server.appwidget.AppWidgetServiceImpl$Provider.id' on a null object reference"
                    //so just return null if that happens.
                    peekLogUtils?.debugLog("Error getting child node", e)
                    null
                } catch (_: IllegalStateException) {
                    try {
                        parentNode.isSealed = true
                        parentNode.getChild(i).also {
                            parentNode.isSealed = false
                        }
                    } catch (e: Exception) {
                        peekLogUtils?.debugLog("Error getting child node", e)
                        null
                    }
                } catch (e: IndexOutOfBoundsException) {
                    peekLogUtils?.debugLog("Error getting child node", e)
                    null
                }

                if (child != null) {
                    if (child.childCount > 0) {
                        addAllNodesToList(child, list, ids, awaits, nodeAddedCallback)
                    } else {
                        list.add(child)
                        nodeAddedCallback(child)
                        if (child.isVisibleToUser && child.viewIdResourceName != null) {
                            ids.add(child.viewIdResourceName)
                        }
                    }
                }
            })
        }
    }

    suspend fun Context.runWindowOperation(
        frameDelegates: Map<Int, MainWidgetFrameDelegate>,
        drawerDelegate: DrawerDelegate,
        getWindows: () -> SparseArray<List<AccessibilityWindowInfo>>?,
        initialRun: Boolean = false,
    ) {
        val isScreenOn = lsDisplayManager.isAnyDisplayOn
        val isOnKeyguard = globalState.wasOnKeyguard.value

        logUtils.debugLog(
            "Trying to run window operation " +
                    "${initialRun}, " +
                    "${isScreenOn}, " +
                    "${isOnKeyguard}, " +
                    "${prefManager.widgetFrameEnabled}, " +
                    "${prefManager.drawerEnabled}, " +
                    "${drawerDelegate.isAttached}, " +
                    "${prefManager.drawerHideWhenNotificationPanelOpen}",
            null,
        )

        //The below block can (very rarely) take over half a second to execute, so only run it
        //if we actually need to (i.e. on the lock screen and screen is on).
        if (initialRun || (isScreenOn &&
                    (((isOnKeyguard || FrameSpecificPreferences.doAnyFramesHaveSettingEnabled(
                        this,
                        PrefManager.KEY_SHOW_IN_NOTIFICATION_CENTER,
                    )) &&
                            prefManager.widgetFrameEnabled /* This is only needed when the frame is enabled */) ||
                            (prefManager.drawerEnabled && drawerDelegate.isAttached && prefManager.drawerHideWhenNotificationPanelOpen)))
        ) {
            logUtils.debugLog(
                "Running window operation.",
                if (initialRun) LogUtils.DefaultException() else null,
            )

            getWindows()?.forEach { displayId, windows ->
                val windowInfo = processWindows(windows).also { windowInfo ->
                    logUtils.debugLog("Got windows for display $displayId: $windowInfo", null)
                }

                val relevantFrameDelegates = frameDelegates.filter { (val frame = value) -> frame.display?.displayId == displayId }

                windowInfo.sysUiWindowViewIds.let { sysUiWindowViewIds ->
                    logUtils.debugLog("Found IDs on display $displayId\n${sysUiWindowViewIds.joinToString("\n")}", null)
                    //Update any ID list widgets on the new IDs
                    coroutineScope {
                        DebugIDsManager.setItems(displayId, sysUiWindowViewIds)
                        launch(Dispatchers.Main) {
                            IDListProvider.sendUpdate(this@runWindowOperation)
                        }
                    }
                }

                if (isDebug) {
                    windowInfo.sysUiWindowNodes.let { sysUiWindowNodes ->
                        logUtils.debugLog(
                            "$displayId: ${sysUiWindowNodes.filter { it.isVisibleToUser }.map { it.viewIdResourceName }}",
                            null,
                        )
                    }
                }

                val notificationsWereOpen = globalState.showingNotificationsPanel.value[displayId]
                val notificationsAreOpen = windowInfo.nodeState.showingNotificationsPanel.value

                //Samsung's Screen-Off Memo is really just a normal Activity that shows over the lock screen.
                //However, it's not an Application-type window for some reason, so it won't hide with the
                //currentAppLayer check. Explicitly check for its existence here.
                globalState.isOnScreenOffMemo[displayId] = isOnKeyguard && windowInfo.hasScreenOffMemoWindow
                globalState.isOnEdgePanel[displayId] = windowInfo.hasEdgePanelWindow || windowInfo.nodeState.showingEdgePanel.value
                globalState.isOnFaceWidgets[displayId] = windowInfo.hasFaceWidgetsWindow
                        || windowInfo.nodeState.onFaceWidgets.value
                //Generate "layer" values for the System UI window and for the topmost app window, if
                //it exists.
                //currentAppLayer *should* be -1 even if there's an app open in the background,
                //since getWindows() is only meant to return windows that can actually be
                //interacted with. The only time it should be anything else (usually 1) is
                //if an app is displaying above the keyguard, such as the incoming call
                //screen or the camera.
                globalState.currentAppLayer[displayId] =
                    if (windowInfo.topAppWindowIndex != -1) windowInfo.windows.size - windowInfo.topAppWindowIndex else windowInfo.topAppWindowIndex
                globalState.currentSysUiLayer[displayId] =
                    if (windowInfo.minSysUiWindowIndex != -1) windowInfo.windows.size - windowInfo.minSysUiWindowIndex else windowInfo.minSysUiWindowIndex
                globalState.currentSystemLayer[displayId] =
                    if (windowInfo.topNonSysUiWindowIndex != -1) windowInfo.windows.size - windowInfo.topNonSysUiWindowIndex else windowInfo.topNonSysUiWindowIndex
                //This is mostly a debug value to see which app LSWidg thinks is on top.
                globalState.currentAppPackage[displayId] = windowInfo.topAppWindowPackageName
                globalState.hidingForPresentApp[displayId] = windowInfo.hasHideForPresentApp
                globalState.onMainLockScreen[displayId] = windowInfo.nodeState.onMainLockscreen.value
                globalState.showingPowerMenu[displayId] = windowInfo.nodeState.showingPowerMenu.value
                globalState.showingSecurityInput[displayId] = windowInfo.nodeState.showingSecurityInput.value
                globalState.accessibilitySeesNotificationsOnMainLockScreen[displayId] = windowInfo.nodeState.onMainLockscreen.value &&
                        windowInfo.nodeState.hasNotificationsShowing.value
                globalState.showingNotificationsPanel[displayId] = notificationsAreOpen
                globalState.notificationsPanelFullyExpanded[displayId] = (windowInfo.nodeState.hasMoreButton.value) || (windowInfo.nodeState.hasSettingsContainerButton.value &&
                        !windowInfo.nodeState.hasClearAllButton.value)
                globalState.hideForPresentIds[displayId] = windowInfo.nodeState.hideForPresentIds.value
                globalState.hideForNonPresentIds[displayId] = windowInfo.nodeState.hideForNonPresentIds.value

                relevantFrameDelegates.forEach { (val frameDelegate = value) ->
                    frameDelegate.updateWindowState(
                        updateAccessibility = true,
                    )
                }

                // Drawer only supports default display right now.
                if (displayId == Display.DEFAULT_DISPLAY &&
                    !drawerDelegate.viewModel.scrollingOpen.value &&
                    notificationsWereOpen != notificationsAreOpen &&
                    notificationsAreOpen &&
                    drawerDelegate.isAttached &&
                    prefManager.drawerHideWhenNotificationPanelOpen
                ) {
                    eventManager.sendEvent(Event.CloseDrawer)
                }

                logUtils.debugLog(
                    "NewState for display $displayId\n" +
                            "${relevantFrameDelegates.values.firstOrNull()?.state}\n" +
                            "${drawerDelegate.state}\n" +
                            "$globalState",
                    null,
                )

                windowInfo.sysUiWindowNodes.forEachParallel { node ->
                    try {
                        node.isSealed = false
                    } catch (_: Throwable) {
                    }

                    try {
                        @Suppress("DEPRECATION")
                        node.recycle()
                    } catch (_: IllegalStateException) {
                    }
                }

                windowInfo.windows.forEachParallel {
                    try {
                        it.root?.isSealed = false
                    } catch (_: Throwable) {
                    }

                    try {
                        @Suppress("DEPRECATION")
                        it.root?.recycle()
                    } catch (_: IllegalStateException) {
                    }
                }
            }
        }
    }

    fun CoroutineScope.runAccessibilityJob(
        context: Context,
        event: AccessibilityEvent,
        frameDelegates: Map<Int, MainWidgetFrameDelegate>,
        drawerDelegate: DrawerDelegate,
        kgm: KeyguardManager,
        imm: InputMethodManager,
        getWindows: () -> SparseArray<List<AccessibilityWindowInfo>>?,
    ) = async(Dispatchers.Main) {
        with(context) {
            logUtils.debugLog("Running accessibility job")

            if (lsDisplayManager.multiDisplaySupported) {
                lsDisplayManager.displayAndWmCache.value.values.forEach { [dis, wm] ->
                    globalState.showingKeyboard[dis?.displayId ?: Display.DEFAULT_DISPLAY] =
                        wm?.currentWindowMetrics
                        ?.windowInsets?.getInsets(WindowInsets.Type.ime())
                        ?.toRect()
                        ?.isEmpty
                        .let { it == false }
                }
            } else {
                globalState.showingKeyboard[Display.DEFAULT_DISPLAY] = try {
                    imm.inputMethodWindowVisibleHeight > 0
                } catch (e: Throwable) {
                    logUtils.debugLog("Unable to check if keyboard is showing, assuming it's not", e)
                    // Fetching the IME height can cause the system to throw an NPE:
                    // "Attempt to read from field 'com.android.server.wm.DisplayFrames com.android.server.wm.DisplayContent.mDisplayFrames' on a null object reference".
                    // If this happens, assume the keyboard isn't showing.
                    false
                }
            }

            //Check if the lock screen is shown.
            globalState.wasOnKeyguard.value = kgm.isKeyguardLocked

            if (isDebug) {
                // Nest this in the debug check so that loop doesn't have to run always.
                try {
                    logUtils.debugLog(
                        "Source Node ID: ${event.sourceNodeId}, Window ID: ${event.windowId}, Source ID Name: ${event.source?.viewIdResourceName}",
                        null
                    )

                    if (event.recordCount > 0) {
                        logUtils.debugLog(
                            "Records: ${
                                run {
                                    val records = ArrayList<String>()
                                    for (i in 0 until event.recordCount) {
                                        val record = event.getRecord(i)
                                        records.add("$record ${record.sourceNodeId} ${record.windowId} ${record.source?.viewIdResourceName}")
                                    }
                                    records.joinToString(",,,,,,,,")
                                }
                            }",
                            null,
                        )
                    }
                } catch (e: Exception) {
                    logUtils.debugLog("Error printing debug info", e)
                }
            }

            logUtils.debugLog(
                "Accessibility event: $event, " +
                        "isScreenOn: ${lsDisplayManager.isAnyDisplayOn}, " +
                        "wasOnKeyguard: ${globalState.wasOnKeyguard.value}, " +
                        "${drawerDelegate.state}",
                null
            )

            if (prefManager.widgetFrameEnabled) {
                frameDelegates.forEach { (val frameDelegate = value) ->
                    frameDelegate.updateStateAndWindowState(
                        updateAccessibility = true,
                        transform = {
                            it.copy(
                                screenOrientation = frameDelegate.screenOrientation ?: it.screenOrientation,
                            )
                        },
                    )
                }
            }

            runWindowOperation(
                frameDelegates = frameDelegates,
                drawerDelegate = drawerDelegate,
                getWindows = getWindows,
            )

            // Some logic for making the drawer go away or system dialogs dismiss when widgets launch Activities indirectly.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                val matchesWindowsChanged =
                    event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED
                            && ((event.windowChanges and AccessibilityEvent.WINDOWS_CHANGE_ADDED != 0)
                            || (event.windowChanges and AccessibilityEvent.WINDOWS_CHANGE_ACTIVE != 0))
                val matchesWindowStateChanged =
                    event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED

                logUtils.debugLog(
                    "Checking for dismiss eligibility.\n" +
                            "matchesWindowsChanges: $matchesWindowsChanged\n" +
                            "matchesWindowStateChanged: $matchesWindowStateChanged\n" +
                            "packageName: ${event.packageName}\n" +
                            "handlingClick: ${globalState.handlingClick.value}",
                    null,
                )

                if ((matchesWindowsChanged || matchesWindowStateChanged)
                    && event.packageName != packageName
                    && globalState.handlingClick.value.isNotEmpty()
                ) {
                    logUtils.debugLog("Starting dismiss Activity because of window change.", null)
                    DismissOrUnlockActivity.launch(context)

                    if (globalState.handlingClick.value.containsKey(DrawerDelegate.ID)) {
                        logUtils.debugLog("Hiding drawer because of window change", null)
                        eventManager.sendEvent(Event.CloseDrawer)
                    }

                    globalState.handlingClick.value = mapOf()
                }
            }

            if ((event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED
                        || event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) && event.packageName != null
            ) {
                globalState.handlingClick.value = mapOf()
            }

            try {
                //Make sure to recycle the copy of the event.
                @Suppress("DEPRECATION")
                event.recycle()
            } catch (_: IllegalStateException) {
                //Sometimes the event is already recycled somehow.
            }

            if (prefManager.widgetFrameEnabled) {
                frameDelegates.forEach { (val frameDelegate = value) ->
                    frameDelegate.updateStateAndWindowState(true)
                }
            }
        }
    }
}

//Check if the Accessibility service is enabled
val Context.isAccessibilityEnabled: Boolean
    get() = Settings.Secure.getString(
        contentResolver,
        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
    )?.contains(ComponentName(this, Accessibility::class.java).flattenToString()) == true

fun Context.openAccessibilitySettings() {
    //Samsung devices have a separate Activity for listing
    //installed Accessibility Services, for some reason.
    //It's exported and permission-free, at least on Android 10,
    //so attempt to launch it. A "dumb" try-catch is simpler
    //than a check for the existence and state of this Activity.
    //If the Installed Services Activity can't be launched,
    //just launch the normal Accessibility Activity.
    try {
        val accIntent = Intent(Intent.ACTION_MAIN)
        accIntent.`package` = "com.android.settings"
        accIntent.component = ComponentName(
            "com.android.settings",
            $$"com.android.settings.Settings$AccessibilityInstalledServiceActivity"
        )
        startActivity(accIntent)
    } catch (e: Exception) {
        logUtils.debugLog("Error opening Installed Services:", e)

        try {
            val accIntent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
            startActivity(accIntent)
        } catch (e: Exception) {
            logUtils.debugLog("Error opening Accessibility Settings", e)

            try {
                val settingsIntent = Intent(Settings.ACTION_SETTINGS)
                startActivity(settingsIntent)
            } catch (e: Exception) {
                logUtils.debugLog("Error opening Settings", e)

                Toast.makeText(this, R.string.unable_to_open_settings, Toast.LENGTH_SHORT).show()
            }
        }
    }
}

fun <T> AccessibilityNodeInfo?.use(block: (AccessibilityNodeInfo?) -> T): T {
    val result = block(this)
    @Suppress("DEPRECATION")
    this?.recycle()
    return result
}

fun AccessibilityEvent.copyCompat(): AccessibilityEvent {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        AccessibilityEvent(this)
    } else {
        @Suppress("DEPRECATION")
        AccessibilityEvent.obtain(this)
    }
}

fun AccessibilityNodeInfo.hasVisibleIds(ids: Map<String, Unit>): Boolean {
    return ids.containsKey(viewIdResourceName) && isVisibleToUser
}

fun AccessibilityNodeInfo.hasWildcardId(id: String): Boolean {
    return viewIdResourceName?.contains(id) == true && isVisibleToUser
}

fun <T> unitMapOf(vararg keys: T?): Map<T, Unit> {
    return keys.filterNotNull().associateWith {}
}
