package tk.zwander.lockscreenwidgets.util

import android.content.Context
import android.content.SharedPreferences
import android.graphics.*
import android.graphics.drawable.Drawable
import android.view.Display
import android.view.Gravity
import android.view.Surface
import android.view.WindowManager
import androidx.compose.animation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.core.graphics.component1
import androidx.core.graphics.component2
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.RecyclerView
import dev.zwander.lazyspannedgrid.rememberLazySpannedGridState
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mx.platacard.pagerindicator.PagerWormIndicator
import tk.zwander.common.activities.DismissOrUnlockActivity
import tk.zwander.common.activities.SelectIconPackActivity
import tk.zwander.common.compose.WidgetGrid
import tk.zwander.common.compose.util.createComposeViewHolder
import tk.zwander.common.compose.util.findAccessibility
import tk.zwander.common.compose.util.rememberBooleanPreferenceState
import tk.zwander.common.compose.util.rememberPreferenceState
import tk.zwander.common.data.provider.IFramePrefsProvider
import tk.zwander.common.data.provider.IFrameProvider
import tk.zwander.common.listeners.WidgetResizeListener
import tk.zwander.common.util.*
import tk.zwander.lockscreenwidgets.activities.add.ReconfigureFrameWidgetActivity
import tk.zwander.lockscreenwidgets.compose.WidgetFrameLayout
import tk.zwander.lockscreenwidgets.data.Mode
import kotlin.time.Duration.Companion.milliseconds

/**
 * Handle most of the logic involving the widget frame.
 */
open class MainWidgetFrameDelegate protected constructor(
    context: Context,
    protected val id: Int = ID,
    initialDisplayId: String,
    override val targetDisplayId: MutableStateFlow<String> = MutableStateFlow(initialDisplayId)
) : BaseDelegate<MainWidgetFrameDelegate.State>(
    context = context,
    targetDisplayId = targetDisplayId,
), IFrameProvider {
    companion object {
        const val ID = -1

        private val instance = MutableStateFlow<MainWidgetFrameDelegate?>(null)

        val readOnlyInstance = instance.asStateFlow()

        @Synchronized
        fun peekInstance(context: Context): MainWidgetFrameDelegate? {
            if (instance.value == null) {
                context.logUtils.debugLog("Accessibility isn't running yet")

                return null
            }

            return instance.value
        }

        @Synchronized
        fun getInstance(context: Context, displayId: String): MainWidgetFrameDelegate {
            return instance.value ?: run {
                val accessibilityContext = context.findAccessibility()

                if (accessibilityContext == null) {
                    throw IllegalStateException("Delegate can only be initialized by Accessibility Service!")
                } else {
                    MainWidgetFrameDelegate(accessibilityContext, initialDisplayId = displayId).also {
                        instance.value = it
                    }
                }
            }
        }

        @Synchronized
        fun invalidateInstance() {
            instance.value = null
        }

        fun allIds(context: Context): List<Int> {
            return [ID] + context.prefManager.currentSecondaryFramesWithStringDisplay.keys
        }
    }

    override val holderId: Int
        get() = id

    override var state = MutableStateFlow(State())

    private val saveMode: FrameSizeAndPosition.FrameType
        get() {
            val isLandscape = prefManager.separateFrameLayoutForLandscape &&
                    (state.value.screenOrientation == Surface.ROTATION_90 ||
                            state.value.screenOrientation == Surface.ROTATION_270)
            val isMainFrame = id == ID

            return when {
                globalState.notificationsPanelFullyExpanded.value[this@MainWidgetFrameDelegate.display?.displayId] == true &&
                        framePrefs.showInNotificationShade -> {
                    if (kgm.isKeyguardLocked && framePrefs.separateLockNCPosition) {
                        if (isMainFrame) {
                            FrameSizeAndPosition.FrameType.LockNotification.select(!isLandscape)
                        } else {
                            FrameSizeAndPosition.FrameType.SecondaryLockNotification.select(
                                !isLandscape,
                                id
                            )
                        }
                    } else {
                        if (isMainFrame) {
                            FrameSizeAndPosition.FrameType.NotificationNormal.select(!isLandscape)
                        } else {
                            FrameSizeAndPosition.FrameType.SecondaryNotification.select(
                                !isLandscape,
                                id
                            )
                        }
                    }
                }

                else -> {
                    if (isMainFrame) {
                        FrameSizeAndPosition.FrameType.LockNormal.select(!isLandscape)
                    } else {
                        FrameSizeAndPosition.FrameType.SecondaryLockscreen.select(!isLandscape, id)
                    }
                }
            }
        }

    //The size, position, and such of the widget frame on the lock screen.
    final override val params by lazy {
        WindowManager.LayoutParams().apply {
            type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY

            this@MainWidgetFrameDelegate.display?.let { display ->
                frameSizeAndPosition.getSizeForType(saveMode, display).let { size ->
                    width = display.dpToPx(size.x)
                    height = display.dpToPx(size.y)
                }
                frameSizeAndPosition.getPositionForType(saveMode, display).let { pos ->
                    x = pos.x
                    y = pos.y
                }
            }

            gravity = Gravity.CENTER

            @Suppress("DEPRECATION")
            flags =
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
                        WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                        WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
            format = PixelFormat.RGBA_8888
        }
    }

    private val animationMutex = Mutex()

    override val rootView by lazy {
        viewModel.createComposeViewHolder {
            WidgetFrameLayout(
                widgetGrid = { modifier ->
                    BoxWithConstraints(
                        modifier = modifier,
                    ) {
                        val constraints = constraints
                        val gridState = rememberLazySpannedGridState()
                        val rowCount by rememberPreferenceState(
                            key = PrefManager.KEY_FRAME_ROW_COUNT,
                            preferences = framePrefs.framePreferences,
                        ) {
                            framePrefs.rowCount
                        }
                        val columnCount by rememberPreferenceState(
                            key = PrefManager.KEY_FRAME_COL_COUNT,
                            preferences = framePrefs.framePreferences,
                        ) {
                            framePrefs.colCount
                        }
                        var currentWidgetsState by rememberPreferenceState(
                            key = FramePrefs.generateCurrentWidgetsKey(id),
                            value = { currentWidgets.toList() },
                            onChanged = { _, value -> currentWidgets = value.toSet() },
                        )

                        val rememberFramePosition by rememberPreferenceState(
                            key = PrefManager.KEY_FRAME_REMEMBER_POSITION,
                        ) {
                            prefManager.rememberFramePosition
                        }
                        var storedPosition by rememberPreferenceState(
                            key = PrefManager.KEY_CURRENT_PAGE,
                            value = { framePrefs.currentIndex },
                            onChanged = { _, value ->
                                framePrefs.currentIndex = value
                            },
                            preferences = framePrefs.framePreferences,
                        )
                        val derivedStoredPosition by remember {
                            derivedStateOf { storedPosition }
                        }
                        val pageIndicatorBehavior by rememberPreferenceState(
                            key = PrefManager.KEY_PAGE_INDICATOR_BEHAVIOR,
                            value = { context.prefManager.pageIndicatorBehavior },
                        )
                        var showingPager by remember(pageIndicatorBehavior) {
                            mutableStateOf(
                                pageIndicatorBehavior != PrefManager.VALUE_PAGE_INDICATOR_BEHAVIOR_HIDDEN,
                            )
                        }
                        val pageInfo by remember(pageIndicatorBehavior) {
                            derivedStateOf {
                                val pageSizePx = gridState.lineSizePx * gridState.mainAxisLineCount.coerceAtLeast(1)
                                val scrollOffsetPx =
                                    gridState.firstVisibleLine * gridState.lineSizePx + gridState.firstVisibleLineScrollOffset
                                val pageOffset =
                                    (scrollOffsetPx % pageSizePx.coerceAtLeast(1)) / constraints.maxWidth.toFloat()
                                        .coerceAtLeast(1f)
                                val page = (scrollOffsetPx / pageSizePx.coerceAtLeast(1))

                                val pageCount = gridState.layoutInfo.totalLineCount / columnCount

                                (page + pageOffset) to (pageCount)
                            }
                        }
                        val frameLocked by rememberBooleanPreferenceState(
                            key = PrefManager.KEY_LOCK_WIDGET_FRAME,
                        )
                        val state by state.collectAsState()

                        LaunchedEffect(pageInfo) {
                            showingPager = pageIndicatorBehavior != PrefManager.VALUE_PAGE_INDICATOR_BEHAVIOR_HIDDEN
                        }

                        LaunchedEffect(showingPager, pageInfo) {
                            if (showingPager && pageIndicatorBehavior == PrefManager.VALUE_PAGE_INDICATOR_BEHAVIOR_AUTO_HIDE) {
                                delay(2000.milliseconds)
                                showingPager = false
                            }
                        }

                        LaunchedEffect(null) {
                            if (rememberFramePosition && gridState.layoutInfo.totalLineCount > 1) {
                                gridState.scrollToLine(derivedStoredPosition)
                            }
                        }

                        LaunchedEffect(gridState.firstVisibleLine) {
                            val newLine = gridState.firstVisibleLine
                            if (newLine != derivedStoredPosition && !gridState.isScrollInProgress) {
                                storedPosition = newLine
                            }
                        }

                        WidgetGrid(
                            currentWidgets = currentWidgetsState,
                            onWidgetsChanged = { widgets ->
                                currentWidgetsState = widgets
                            },
                            orientation = Orientation.Horizontal,
                            columnCount = columnCount,
                            rowCount = rowCount,
                            resizeThresholdPx = { which ->
                                val display = viewModel.display.orDefault(context)
                                val frameSize = frameSizeAndPosition.getSizeForType(viewModel.saveMode, display)
                                if (which == WidgetResizeListener.Which.LEFT || which == WidgetResizeListener.Which.RIGHT) {
                                    display.dpToPx(frameSize.x.toInt()) / colCount
                                } else {
                                    display.dpToPx(frameSize.y.toInt()) / rowCount
                                }
                            },
                            launchAddActivity = {
                                context.eventManager.sendEvent(Event.LaunchAddWidget(holderId))
                            },
                            launchReconfigure = { id, providerInfo ->
                                ReconfigureFrameWidgetActivity.launch(context, id, holderId, providerInfo)
                            },
                            launchShortcutIconOverride = { id ->
                                SelectIconPackActivity.launchForOverride(context, id)
                            },
                            modifier = Modifier.fillMaxSize(),
                            rowSpanForAddButton = 1,
                            enableSnapping = true,
                            lazyGridState = gridState,
                            locked = frameLocked && !state.isPreview,
                            itemSpacingKey = PrefManager.KEY_FRAME_ITEM_SPACING,
                            preferences = framePrefs.framePreferences,
                        )

                        AnimatedVisibility(
                            visible = showingPager,
                            enter = fadeIn() + slideInVertically(initialOffsetY = { it }),
                            exit = fadeOut() + slideOutVertically(targetOffsetY = { it }),
                            modifier = Modifier
                                .align(Alignment.BottomCenter)
                                .padding(bottom = 8.dp),
                        ) {
                            PagerWormIndicator(
                                pageCount = pageInfo.second,
                                currentPageFraction = remember {
                                    derivedStateOf {
                                        pageInfo.first
                                    }
                                },
                                activeDotColor = LocalContentColor.current,
                                dotColor = LocalContentColor.current.copy(alpha = 0.5f),
                            )
                        }
                    }
                },
                modifier = Modifier.fillMaxSize(),
            )
        }.also { it.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) }
    }

    override val viewModel = WidgetFrameViewModel(this)

    override val prefsHandler = HandlerRegistry {
        handler(PrefManager.KEY_FRAME_MASKED_MODE) {
            updateWallpaperLayerIfNeeded()
        }
        handler(PrefManager.KEY_SHOW_IN_NOTIFICATION_CENTER) {
            updateState { it.copy(isPendingNotificationStateChange = true) }
        }
        handler(
            PrefManager.KEY_LOCK_WIDGET_FRAME,
        ) {
            viewModel.currentEditingInterfaceId.value = RecyclerView.NO_POSITION
        }
        handler(PrefManager.KEY_WIDGET_FRAME_ENABLED) {
            lifecycleScope.launch {
                updateState {
                    it.copy(isPreview = false)
                }
                updateWindowState()
            }
        }
        handler(PrefManager.KEY_CAN_SHOW_FRAME_FROM_TASKER, PrefManager.KEY_FORCE_SHOW_FRAME) {
            lifecycleScope.launch {
                updateWindowState()
            }
        }
        handler(PrefManager.KEY_PRIMARY_FRAME_DISPLAY) {
            val display = prefManager.primaryFrameDisplay
            targetDisplayId.value = display

            removeWindow()

            if (canShow()) {
                addWindow()
            }
        }
        handler(PrefManager.KEY_FRAME_IGNORE_TOUCHES) {
            updateState {
                it.copy(ignoreAllTouches = framePrefs.ignoreAllTouches)
            }
        }
        handler(PrefManager.KEY_FRAME_MASKED_MODE) {
            if (framePrefs.maskedMode) {
                wallpaperClient.tryBindService()
            }
        }
    }

    private val showWallpaperLayerCondition: Boolean
        get() = !state.value.isPreview &&
                framePrefs.maskedMode &&
                (globalState.notificationsPanelFullyExpanded.value[this@MainWidgetFrameDelegate.display?.displayId] == false ||
                        !framePrefs.showInNotificationShade) &&
                (globalState.showingNotificationsPanel.value[this@MainWidgetFrameDelegate.display?.displayId] == false ||
                        framePrefs.hideOnNotificationShade)

    override suspend fun onEvent(event: Event) {
        super.onEvent(event)

        when (event) {
            is Event.FrameMoveFinished -> {
                if (event.frameId == id) {
                    updateWallpaperLayerIfNeeded()
                }
            }

            is Event.TempHide -> {
                if (event.frameId == id) {
                    updateState { it.copy(isTempHide = true) }
                    updateWindowState()
                }
            }

            is Event.CenterFrameHorizontally -> {
                if (event.frameId == id) {
                    this@MainWidgetFrameDelegate.display?.let { display ->
                        frameSizeAndPosition.setPositionForType(
                            saveMode,
                            Point(
                                0,
                                frameSizeAndPosition.getPositionForType(saveMode, display).y
                            )
                        )
                    }
                    updateWindow()
                }
            }

            is Event.CenterFrameVertically -> {
                if (event.frameId == id) {
                    this@MainWidgetFrameDelegate.display?.let { display ->
                        frameSizeAndPosition.setPositionForType(
                            saveMode,
                            Point(
                                frameSizeAndPosition.getPositionForType(saveMode, display).x,
                                0
                            )
                        )
                    }
                    updateWindow()
                }
            }

            is Event.FrameResized -> {
                if (event.frameId == id) {
                    when (event.which) {
                        Event.FrameResized.Side.LEFT -> {
                            params.width -= event.velocity
                            params.x += (event.velocity / 2)
                        }

                        Event.FrameResized.Side.TOP -> {
                            params.height -= event.velocity
                            params.y += (event.velocity / 2)
                        }

                        Event.FrameResized.Side.RIGHT -> {
                            params.width += event.velocity
                            params.x += (event.velocity / 2)
                        }

                        Event.FrameResized.Side.BOTTOM -> {
                            params.height += event.velocity
                            params.y += (event.velocity / 2)
                        }
                    }

                    updateOverlay()

                    frameSizeAndPosition.setPositionForType(
                        saveMode,
                        Point(params.x, params.y)
                    )
                    this@MainWidgetFrameDelegate.display?.let { display ->
                        frameSizeAndPosition.setSizeForType(
                            saveMode,
                            PointF(display.pxToDp(params.width), display.pxToDp(params.height))
                        )
                    }

                    if (event.isUp) {
                        eventManager.sendEvent(Event.FrameResizeFinished(id))
                        updateWallpaperLayerIfNeeded()
                    }
                }
            }

            is Event.FrameMoved -> {
                if (event.frameId == id) {
                    params.x += event.velX.toInt()
                    params.y += event.velY.toInt()

                    updateOverlay()

                    frameSizeAndPosition.setPositionForType(
                        saveMode,
                        Point(params.x, params.y)
                    )
                }
            }

            is Event.FrameIntercept -> {
                if (event.frameId == id) {
                    forceWakelock(event.down)
                }
            }

            is Event.PreviewFrames -> {
                updateState {
                    val isPreview = event.show == Event.PreviewFrames.ShowMode.SHOW ||
                            (event.show == Event.PreviewFrames.ShowMode.TOGGLE && !it.isPreview)
                    it.copy(
                        isPreview = isPreview,
                        isTempHide = !isPreview && it.isTempHide,
                    )
                }
            }

            else -> {}
        }
    }

    override fun onWidgetClick(trigger: Boolean): Boolean {
        val ignoreTouches = framePrefs.ignoreWidgetTouches || globalState.itemIsActive.value

        if (!ignoreTouches) {
            if (trigger && prefManager.requestUnlock && prefManager.frameDirectlyCheckForActivity) {
                DismissOrUnlockActivity.launch(this)
            } else {
                if (prefManager.requestUnlock) {
                    globalState.handlingClick[id] = Unit
                }
            }
        }

        return !ignoreTouches
    }

    override fun onCreate() {
        super.onCreate()

        if (created) {
            return
        }

        viewModel.viewModelScope.launch(Dispatchers.Main) {
            lsDisplayManager.displayPowerStates
                .map { it.displayStates[this@MainWidgetFrameDelegate.display?.uniqueIdCompat] != false }
                .collect { isScreenOn ->
                    if (!isScreenOn) {
                        this@MainWidgetFrameDelegate.display?.displayId?.let {
                            globalState.notificationsPanelFullyExpanded[it] = false
                        }
                        updateState { it.copy(isPreview = false, isTempHide = false) }

                        //If the device has some sort of AOD or ambient display, by the time we receive
                        //an accessibility event and see that the display is off, it's usually too late
                        //and the current screen content has "frozen," causing the widget frame to show
                        //where it shouldn't. ACTION_SCREEN_OFF is called early enough that we can remove
                        //the frame before it's frozen in place.
                        forceWakelock(false)
                        updateStateAndWindowState(
                            transform = {
                                it.copy(
                                    isTempHide = false,
                                    isPreview = false,
                                )
                            },
                        )
                    } else {
                        updateWindowState()
                    }
                }
        }

        viewModel.viewModelScope.launch(Dispatchers.Main) {
            var oldState = state.value
            state.collect { newState ->
                var actualNewState = newState

                if (actualNewState.isTempHide != oldState.isTempHide && actualNewState.isTempHide) {
                    actualNewState = actualNewState.copy(isPreview = false)
                }

                if (actualNewState.screenOrientation != oldState.screenOrientation) {
                    actualNewState =
                        actualNewState.copy(isPendingOrientationStateChange = true, isPreview = false)
                }

                // ------------ //

                if (actualNewState.isPreview != oldState.isPreview) {
                    lifecycleScope.launch(Dispatchers.Main) {
                        if (actualNewState.isPreview) {
                            if (canShow()) {
                                addWindow()
                            }
                        } else {
                            if (!canShow()) {
                                removeWindow()
                            }
                        }
                    }
                }

                if (actualNewState.ignoreAllTouches != oldState.ignoreAllTouches) {
                    lifecycleScope.launch(Dispatchers.Main) {
                        updateIgnoreAllTouches()
                    }
                }

                oldState = actualNewState
                state.value = actualNewState
            }
        }

        viewModel.viewModelScope.launch(Dispatchers.Main) {
            globalState.notificationsPanelFullyExpanded.collect {
                updateState { it.copy(isPendingNotificationStateChange = true, isPreview = false) }
            }
        }

        viewModel.viewModelScope.launch(Dispatchers.Main) {
            globalState.notificationCount.collect {
                //Receive updates from our notification listener service on how many
                //notifications are currently shown to the user. This count excludes
                //notifications not visible on the lock screen.
                //If the notification count is > 0, and the user has the option enabled,
                //make sure to hide the widget frame.
                updateWindowState()
            }
        }

        updateState {
            it.copy(
                ignoreAllTouches = framePrefs.ignoreAllTouches,
            )
        }

        created = true
    }

    override suspend fun onDestroy() {
        if (lifecycleRegistry.currentState >= Lifecycle.State.INITIALIZED) {
            removeWindow()
        }

        super.onDestroy()

        if (id == ID) {
            invalidateInstance()
        }
    }

    suspend fun updateStateAndWindowState(
        updateAccessibility: Boolean = false,
        transform: (State) -> State = { it },
    ) {
        updateState(transform)
        updateWindowState(updateAccessibility)
    }

    private suspend fun addWindow() {
        logUtils.debugLog("Adding overlay", extras = mapOf("frameId" to id, "rootAttachedBefore" to rootView.isAttachedToWindow))

        withContext(Dispatchers.Main) {
            if (!rootView.isAttachedToWindow) {
                updateWindow()
            }

            logUtils.debugLog("Trying to add overlay ${viewModel.animationState.value}", extras = mapOf(
                "frameId" to id,
                "rootAttachedBefore" to rootView.isAttachedToWindow,
                "animState" to viewModel.animationState.toString(),
            ))

            if (!rootView.isAttachedToWindow && viewModel.animationState.value != AnimationState.STATE_ADDING) {
                logUtils.debugLog("Actually adding overlay", extras = mapOf(
                    "frameId" to id,
                    "displayId" to this@MainWidgetFrameDelegate.display?.uniqueIdCompat,
                    "paramsWidth" to params.width,
                    "paramsHeight" to params.height,
                ))

                viewModel.animationState.value = AnimationState.STATE_ADDING

                val addSuccess = animationMutex.withLock {
                    wm?.safeAddView(rootView, params) == true
                }

                if (addSuccess) {
                    logUtils.debugLog("Overlay added successfully", extras = mapOf("frameId" to id))
                    rootView.awaitNextDraw()
                    rootView.fadeAndScaleIn(DrawerOrFrame.FRAME)
                } else {
                    logUtils.debugLog("Adding overlay failed", extras = mapOf(
                        "frameId" to id,
                        "animState" to viewModel.animationState.toString(),
                    ))
                }

                viewModel.animationState.value = AnimationState.STATE_IDLE
            }
        }
    }

    private suspend fun removeWindow() {
        if (isAttached) {
            logUtils.debugLog("Removing overlay", extras = mapOf("frameId" to id, "rootAttachedBefore" to rootView.isAttachedToWindow))
        }

        if (rootView.isAttachedToWindow && viewModel.animationState.value != AnimationState.STATE_REMOVING) {
            logUtils.debugLog("Trying to remove overlay ${viewModel.animationState.value}", extras = mapOf(
                "frameId" to id,
                "rootAttachedBefore" to rootView.isAttachedToWindow,
                "animState" to viewModel.animationState.toString(),
            ))

            if (!rootView.isAttachedToWindow) {
                logUtils.debugLog("Root view already detached during removeWindow", extras = mapOf("frameId" to id))
                return
            }

            viewModel.animationState.value = AnimationState.STATE_REMOVING

            updateIgnoreAllTouches(true)

            withContext(Dispatchers.Main + NonCancellable) {
                logUtils.debugLog("Actually removing overlay", extras = mapOf(
                    "frameId" to id,
                    "displayId" to this@MainWidgetFrameDelegate.display?.uniqueIdCompat,
                ))

                viewModel.currentEditingInterfaceId.value = RecyclerView.NO_POSITION

                globalState.handlingClick.remove(id)
                forceWakelock(on = false, updateOverlay = false)

                animationMutex.withLock {
                    rootView.fadeAndScaleOut(DrawerOrFrame.FRAME)
                    wm?.safeRemoveViewImmediate(rootView)
                }

                logUtils.debugLog("Overlay removed successfully", extras = mapOf("frameId" to id))
            }

            updateIgnoreAllTouches()

            viewModel.animationState.value = AnimationState.STATE_IDLE
        } else if (!rootView.isAttachedToWindow) {
            logUtils.debugLog("Attempting emergency removeViewImmediate", extras = mapOf("frameId" to id))
            wm?.safeRemoveViewImmediate(rootView, false)
        }
    }

    private fun onFrameAttachmentStateChanged(attached: Boolean) {
        if (lifecycleRegistry.currentState == Lifecycle.State.DESTROYED) {
            logUtils.debugLog("Already destroyed, not handling attachment state", null)
            return
        }

        try {
            if (attached) {
                updateWallpaperLayerIfNeeded()
            }
        } catch (e: NullPointerException) {
            //The stupid "Attempt to read from field 'com.android.server.appwidget.AppWidgetServiceImpl$ProviderId
            //com.android.server.appwidget.AppWidgetServiceImpl$Provider.id' on a null object reference"
            //Exception is thrown on stopListening() as well for some reason.
            logUtils.debugLog("Error handling attachment state", e)
        }
    }

    suspend fun updateWindowState(updateAccessibility: Boolean = false) {
        lifecycleScope.launch {
            if (canShow()) {
                if (updateAccessibility) updateAccessibilityPass()
                addWindow()
            } else {
                removeWindow()
                if (updateAccessibility) updateAccessibilityPass()
            }
        }.join()
    }

    /**
     * Check if the widget frame should show onscreen. There are quite a few conditions for this.
     * This method attempts to check those conditions in increasing order of intensiveness (check simple
     * members first, then try SharedPreferences, then use IPC methods).
     *
     * The widget frame can only show if ALL of the following conditions are met:
     *
     * =======
     * - [saveMode] is [Mode.PREVIEW]
     * =======
     * OR
     * =======
     * - The display this frame is hosted on is fully on.
     * - [State.isTempHide] is false
     * - [GlobalState.notificationsPanelFullyExpanded] is true AND [FrameSpecificPreferences.showInNotificationShade] is true
     * - [GlobalState.hideForPresentIds] is false
     * - [GlobalState.hideForNonPresentIds] is false
     * - [PrefManager.widgetFrameEnabled] is true
     * - [PrefManager.hideInLandscape] is false OR [State.screenOrientation] represents a portrait rotation
     * =======
     * OR
     * =======
     * - [GlobalState.wasOnKeyguard] is true
     * - The display this frame is hosted on is fully on (i.e. the display is properly on: not in Doze or on the AOD)
     * - [State.isTempHide] is false
     * - [FrameSpecificPreferences.showOnMainLockScreen] is true OR [FrameSpecificPreferences.showInNotificationShade] is false
     * - [FrameSpecificPreferences.hideOnFaceWidgets] is false OR [GlobalState.isOnFaceWidgets] is false
     * - [GlobalState.currentAppLayer] is less than 0 (i.e. doesn't exist)
     * - [GlobalState.isOnEdgePanel] is false OR [FrameSpecificPreferences.hideOnEdgePanel] is false
     * - [GlobalState.isOnScreenOffMemo] is false
     * - [GlobalState.onMainLockScreen] is true
     * - [GlobalState.showingSecurityInput] is false OR [FrameSpecificPreferences.hideOnSecurityPage] is false
     * - [GlobalState.showingNotificationsPanel] is false OR [FrameSpecificPreferences.hideOnNotificationShade] is false (OR [GlobalState.notificationsPanelFullyExpanded] is true AND [FrameSpecificPreferences.showInNotificationShade] is true
     * - [GlobalState.notificationCount] is 0 (i.e. no notifications shown on lock screen, not necessarily no notifications at all) OR [FrameSpecificPreferences.hideOnNotifications] is false
     * - [GlobalState.hideForPresentIds] is false OR [PrefManager.presentIds] is empty
     * - [GlobalState.hideForNonPresentIds] is false OR [PrefManager.nonPresentIds] is empty
     * - [PrefManager.hideInLandscape] is false OR [State.screenOrientation] represents a portrait rotation
     * - [PrefManager.widgetFrameEnabled] is true (i.e. the widget frame is actually enabled)
     * =======
     */
    private fun canShow(): Boolean {
        fun forPreview(): Boolean {
            return state.value.isPreview && !state.value.isTempHide && prefManager.widgetFrameEnabled
        }

        fun forCommon(): Boolean {
            return lsDisplayManager.displayPowerStates.value.displayStates[this@MainWidgetFrameDelegate.display?.uniqueIdCompat] == true
                    && !state.value.isTempHide
                    && globalState.hideForPresentIds.value[this@MainWidgetFrameDelegate.display?.displayId] != true
                    && globalState.hideForNonPresentIds.value[this@MainWidgetFrameDelegate.display?.displayId] != true
                    && prefManager.widgetFrameEnabled
                    && (!prefManager.hideInLandscape || state.value.screenOrientation == Surface.ROTATION_0 || state.value.screenOrientation == Surface.ROTATION_180)
                    && framePrefs.canShowFromTasker
                    && (!framePrefs.hideWhenKeyboardShown || globalState.showingKeyboard.value[this@MainWidgetFrameDelegate.display?.displayId] != true)
        }

        fun forSecondaryDisplay(): Boolean {
            return globalState.wasOnKeyguard.value && this@MainWidgetFrameDelegate.display?.displayId != Display.DEFAULT_DISPLAY && forCommon()
        }

        fun forNotificationCenter(): Boolean {
            return (globalState.notificationsPanelFullyExpanded.value[this@MainWidgetFrameDelegate.display?.displayId] == true
                    && framePrefs.showInNotificationShade)
                    && forCommon()
        }

        fun forLockscreen(): Boolean {
            return globalState.wasOnKeyguard.value
                    && (framePrefs.showOnMainLockScreen || !framePrefs.showInNotificationShade)
                    && (!framePrefs.hideOnFaceWidgets || globalState.isOnFaceWidgets.value[this@MainWidgetFrameDelegate.display?.displayId] != true)
                    && ((globalState.currentAppLayer.value[this@MainWidgetFrameDelegate.display?.displayId] ?: 0) < 0 &&
                    globalState.currentAppPackage.value[this@MainWidgetFrameDelegate.display?.displayId] == null)
                    && (globalState.isOnEdgePanel.value[this@MainWidgetFrameDelegate.display?.displayId] != true || !framePrefs.hideOnEdgePanel)
                    && globalState.isOnScreenOffMemo.value[this@MainWidgetFrameDelegate.display?.displayId] != true
                    && (globalState.showingNotificationsPanel.value[this@MainWidgetFrameDelegate.display?.displayId] != true ||
                    !framePrefs.hideOnNotificationShade)
                    && (globalState.showingSecurityInput.value[this@MainWidgetFrameDelegate.display?.displayId] != true ||
                    !framePrefs.hideOnSecurityPage)
                    && (globalState.notificationCount.value == 0 || !framePrefs.hideOnNotifications)
                    && globalState.hidingForPresentApp.value[this@MainWidgetFrameDelegate.display?.displayId] != true
                    && globalState.showingPowerMenu.value[this@MainWidgetFrameDelegate.display?.displayId] != true
                    && forCommon()
        }

        fun forced(): Boolean {
            return prefManager.widgetFrameEnabled && framePrefs.forceShow
        }

        return (forced() || forSecondaryDisplay() || forPreview() || forNotificationCenter() || forLockscreen()).also {
            logUtils.debugLog(
                "canShow $id: $it\n" +
                        "forced: ${forced()}, forSecondaryDisplay: ${forSecondaryDisplay()}, forPreview: ${forPreview()}, forNotificationCenter: ${forNotificationCenter()}, forLockscreen: ${forLockscreen()}, forCommon: ${forCommon()}\n" +
                        "state: ${state.value}\n" +
                        "globalState: $globalState\n" +
                        "showOnMainLockScreen: ${framePrefs.showOnMainLockScreen}\n" +
                        "widgetFrameEnabled: ${prefManager.widgetFrameEnabled}\n" +
                        "hideOnSecurityPage: ${framePrefs.hideOnSecurityPage}\n" +
                        "hideOnNotifications: ${framePrefs.hideOnNotifications}\n" +
                        "hideOnNotificationShade: ${framePrefs.hideOnNotificationShade}\n" +
                        "presentIds: ${prefManager.presentIds}\n" +
                        "nonPresentIds: ${prefManager.nonPresentIds}\n" +
                        "hideInLandscape: ${prefManager.hideInLandscape}\n" +
                        "showInNotificationCenter: ${framePrefs.showInNotificationShade}\n" +
                        "hideOnEdgePanel: ${framePrefs.hideOnEdgePanel}\n" +
                        "canShowFrameFromTasker: ${framePrefs.canShowFromTasker}\n" +
                        "forceShowFrame: ${framePrefs.forceShow}\n" +
                        "hideOnFaceWidgets: ${framePrefs.hideOnFaceWidgets}\n" +
                        "hideWhenKeyboardShown: ${framePrefs.hideWhenKeyboardShown}\n" +
                        "displayPower: ${lsDisplayManager.displayPowerStates.value.displayStates[this@MainWidgetFrameDelegate.display?.uniqueIdCompat]}\n" +
                        "showingPowerMenu: ${globalState.showingPowerMenu.value[this@MainWidgetFrameDelegate.display?.displayId]}\n",
                null,
            )
        }
    }

    /**
     * Compute and draw the appropriate portion of the wallpaper as the widget background,
     * if masked mode is enabled.
     *
     * TODO: this doesn't work properly on a lot of devices. It seems to be something to do with the scale.
     * TODO: I don't know enough about [Matrix]es to fix it.
     *
     * TODO: There are also a lot of limitations related to wallpaper offsets. It doesn't seem to be
     * TODO: possible to retrieve those offsets unless you're the active wallpaper, so this method
     * TODO: just assumes that a wallpaper [Bitmap] that's larger than the screen is centered,
     * TODO: which isn't always true.
     *
     * TODO: You can only specifically retrieve the lock screen wallpaper on Nougat and up.
     */
    private fun updateWallpaperLayerIfNeeded() {
        val display = this@MainWidgetFrameDelegate.display ?: run {
            logUtils.debugLog("Couldn't find display for $targetDisplayId to update wallpaper", null)
            return
        }

        val showWallpaperLayer = showWallpaperLayerCondition

        logUtils.debugLog("updateWallpaperLayerIfNeeded() called $showWallpaperLayer")

        val wallpaperInfo = if (showWallpaperLayer) {
            logUtils.debugLog("Trying to retrieve wallpaper", null)

            try {
                val drawable = wallpaperUtils.getWallpaperDrawable(id)

                logUtils.debugLog("Retrieved wallpaper: $drawable", null)

                drawable?.mutate()?.let {
                    logUtils.debugLog("Setting wallpaper drawable.", null)

                    val realSize = display.rotatedRealSize
                    val loc = rootView.locationOnScreen ?: intArrayOf(0, 0)

                    val dWidth: Int = it.intrinsicWidth
                    val dHeight: Int = it.intrinsicHeight

                    val wallpaperAdjustmentX = (dWidth - realSize.x) / 2f
                    val wallpaperAdjustmentY = (dHeight - realSize.y) / 2f

                    val dx = (-loc[0].toFloat() - wallpaperAdjustmentX)
                    //TODO: a bunch of skins don't like this
                    val dy = (-loc[1].toFloat() - wallpaperAdjustmentY)

                    WallpaperInfo(
                        drawable = it,
                        dx = dx,
                        dy = dy,
                    )
                }
            } catch (e: Exception) {
                logUtils.normalLog("Error setting wallpaper", e)
                null
            }
        } else {
            logUtils.debugLog("Removing wallpaper", null)
            null
        }

        viewModel.wallpaperInfo.value = wallpaperInfo
    }

    /**
     * This is called by the Accessibility Service on an event,
     * after all other logic has finished (e.g. frame removal/addition
     * calls). Use this method for updating things that are conditional
     * upon the frame's state after an event.
     */
    private suspend fun updateAccessibilityPass() {
        if (viewModel.animationState.value == AnimationState.STATE_IDLE) {
            if (state.value.isPendingNotificationStateChange || state.value.isPendingOrientationStateChange) {
                updateWindow()
                updateState {
                    it.copy(
                        isPendingNotificationStateChange = false,
                        isPendingOrientationStateChange = false,
                    )
                }
            }
        }
    }

    /**
     * Update the frame's params for its current state (normal
     * or in expanded notification center).
     */
    override suspend fun updateWindow() {
        val display = this@MainWidgetFrameDelegate.display ?: run {
            logUtils.debugLog("Couldn't find display for $targetDisplayId to update window", null)
            return
        }

        logUtils.debugLog("Checking if params need to be updated")

        logUtils.debugLog(
            "Possibly updating params with display size ${display.rotatedRealSize}",
            null
        )

        val [newX, newY] = frameSizeAndPosition.getPositionForType(saveMode, display)
        val [newW, newH] = frameSizeAndPosition.getSizeForType(saveMode, display).run {
            Point(display.dpToPx(x), display.dpToPx(y))
        }

        var changed = false

        if (params.x != newX) {
            logUtils.debugLog("x changed", null)

            changed = true
            params.x = newX
        }

        if (params.y != newY) {
            logUtils.debugLog("y changed", null)

            changed = true
            params.y = newY
        }

        if (params.width != newW) {
            logUtils.debugLog("w changed", null)

            changed = true
            params.width = newW
        }

        if (params.height != newH) {
            logUtils.debugLog("h changed", null)

            changed = true
            params.height = newH
        }

        if (changed) {
            logUtils.debugLog("Updating params", null)

            updateOverlay()
            updateWallpaperLayerIfNeeded()
        }
    }

    override fun onRootViewAttached() {
        super.onRootViewAttached()

        rootView.post {
            onFrameAttachmentStateChanged(true)
        }
    }

    override fun onRootViewDetached() {
        viewModel.isEditing.value = false
        viewModel.isRemoving.value = false

        super.onRootViewDetached()

        onFrameAttachmentStateChanged(false)
    }

    private suspend fun updateIgnoreAllTouches(override: Boolean = false) {
        params.flags = if ((state.value.ignoreAllTouches && !state.value.isPreview) || override) {
            params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
        } else {
            params.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv()
        }
        updateOverlay()
    }

    data class State(
        val isPreview: Boolean = false,
        //This is used to track when the notification shade has
        //been expanded/collapsed or when the "show in NC" setting
        //has been changed. Since the params are different for the
        //widget frame depending on whether it's showing in the NC
        //or not, we need to update them. We could do it directly,
        //but that causes weird shifting and resizing since it happens
        //before the Accessibility Service hides or shows the frame.
        //So instead, we set this flag to true when the params should be
        //updated. The Accessibility Service takes care of calling
        //the update method after it starts a frame removal or addition.
        //The method itself checks whether it can run (i.e. the
        //animation state of the frame is IDLE) and then updates
        //the params.
        val isPendingNotificationStateChange: Boolean = false,
        val isPendingOrientationStateChange: Boolean = false,
        val isTempHide: Boolean = false,
        val screenOrientation: Int = Surface.ROTATION_0,
        val ignoreAllTouches: Boolean = false,
    )

    open class WidgetFrameViewModel(delegate: MainWidgetFrameDelegate) :
        IWidgetFrameViewModel<State, MainWidgetFrameDelegate>(delegate) {
        val wallpaperInfo = MutableStateFlow<WallpaperInfo?>(null)
        val animationState = MutableStateFlow(AnimationState.STATE_IDLE)
        val acknowledgedTwoFingerTap = MutableStateFlow<Boolean?>(null)
        val acknowledgedThreeFingerTap = MutableStateFlow(false)
        val isAdjustingMask = MutableStateFlow(false)
        val isEditing = MutableStateFlow(false)
        val isRemoving = MutableStateFlow(false)

        override val containerCornerRadiusKey: String = PrefManager.KEY_FRAME_CORNER_RADIUS
        override val widgetCornerRadiusKey: String = PrefManager.KEY_FRAME_WIDGET_CORNER_RADIUS
        override val ignoreWidgetTouchesKey: Pair<String, SharedPreferences>
            get() = PrefManager.KEY_FRAME_IGNORE_WIDGET_TOUCHES to framePrefs.framePreferences
        override val doubleTapTurnOffDisplayKey: String =
            PrefManager.KEY_DOUBLE_TAP_EMPTY_FRAME_SPACE_TURN_OFF_DISPLAY

        override val saveMode: FrameSizeAndPosition.FrameType
            get() = delegate.saveMode
    }

    abstract class IWidgetFrameViewModel<
            State : Any,
            Delegate : BaseDelegate<State>
            >(delegate: Delegate) : BaseViewModel<State, Delegate>(delegate), IFramePrefsProvider {
        abstract val saveMode: FrameSizeAndPosition.FrameType

        override val containerCornerRadiusKey: String = PrefManager.KEY_FRAME_CORNER_RADIUS
        override val widgetCornerRadiusKey: String = PrefManager.KEY_FRAME_WIDGET_CORNER_RADIUS
    }

    data class WallpaperInfo(
        val drawable: Drawable?,
        val dx: Float,
        val dy: Float,
    )

    enum class AnimationState {
        STATE_ADDING,
        STATE_REMOVING,
        STATE_IDLE,
    }
}
