/*
 * 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.statusbar.notification.collection.coordinator

import android.app.smartspace.SmartspaceTarget
import android.os.Parcelable
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject

/**
 * Hides notifications on the lockscreen if the content of those notifications is also visible
 * in smartspace. This ONLY hides the notifications on the lockscreen: if the user pulls the shade
 * down or unlocks the device, then the notifications are unhidden.
 *
 * In addition, notifications that have recently alerted aren't filtered. Tracking this in a way
 * that involves the fewest pipeline invalidations requires some unfortunately complex logic.
 */
@CoordinatorScope
class SmartspaceDedupingCoordinator @Inject constructor(
    private val statusBarStateController: SysuiStatusBarStateController,
    private val smartspaceController: LockscreenSmartspaceController,
    private val notifPipeline: NotifPipeline,
    @Main private val executor: DelayableExecutor,
    private val clock: SystemClock
) : Coordinator {
    private var isOnLockscreen = false

    private var trackedSmartspaceTargets = mutableMapOf<String, TrackedSmartspaceTarget>()

    override fun attach(pipeline: NotifPipeline) {
        pipeline.addPreGroupFilter(filter)
        pipeline.addCollectionListener(collectionListener)
        statusBarStateController.addCallback(statusBarStateListener)
        smartspaceController.addListener(this::onNewSmartspaceTargets)

        recordStatusBarState(statusBarStateController.state)
    }

    private fun isDupedWithSmartspaceContent(entry: NotificationEntry): Boolean {
        return trackedSmartspaceTargets[entry.key]?.shouldFilter ?: false
    }

    private val filter = object : NotifFilter("SmartspaceDedupingFilter") {
        override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
            return isOnLockscreen && isDupedWithSmartspaceContent(entry)
        }
    }

    private val collectionListener = object : NotifCollectionListener {
        override fun onEntryAdded(entry: NotificationEntry) {
            trackedSmartspaceTargets[entry.key]?.let {
                updateFilterStatus(it)
            }
        }

        override fun onEntryUpdated(entry: NotificationEntry) {
            trackedSmartspaceTargets[entry.key]?.let {
                updateFilterStatus(it)
            }
        }

        override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
            trackedSmartspaceTargets[entry.key]?.let { trackedTarget ->
                cancelExceptionTimeout(trackedTarget)
            }
        }
    }

    private val statusBarStateListener = object : StatusBarStateController.StateListener {
        override fun onStateChanged(newState: Int) {
            recordStatusBarState(newState)
        }
    }

    private fun onNewSmartspaceTargets(targets: List<Parcelable>) {
        var changed = false
        val newMap = mutableMapOf<String, TrackedSmartspaceTarget>()
        val oldMap = trackedSmartspaceTargets

        for (target in targets) {
            // For all targets that are SmartspaceTargets and have non-null sourceNotificationKeys
            (target as? SmartspaceTarget)?.sourceNotificationKey?.let { key ->
                val trackedTarget = oldMap.getOrElse(key) {
                    TrackedSmartspaceTarget(key)
                }
                newMap[key] = trackedTarget
                changed = changed || updateFilterStatus(trackedTarget)
            }
            // Currently, only filter out the first target
            break
        }

        for (prevKey in oldMap.keys) {
            if (!newMap.containsKey(prevKey)) {
                oldMap[prevKey]?.cancelTimeoutRunnable?.run()
                changed = true
            }
        }

        if (changed) {
            filter.invalidateList("onNewSmartspaceTargets")
        }

        trackedSmartspaceTargets = newMap
    }

    /**
     * Returns true if the target's alert exception status has changed
     */
    private fun updateFilterStatus(target: TrackedSmartspaceTarget): Boolean {
        val prevShouldFilter = target.shouldFilter

        val entry = notifPipeline.getEntry(target.key)
        if (entry != null) {
            updateAlertException(target, entry)

            target.shouldFilter = !hasRecentlyAlerted(entry)
        }

        return target.shouldFilter != prevShouldFilter && isOnLockscreen
    }

    private fun updateAlertException(target: TrackedSmartspaceTarget, entry: NotificationEntry) {
        val now = clock.currentTimeMillis()
        val alertExceptionExpires = entry.ranking.lastAudiblyAlertedMillis + ALERT_WINDOW

        if (alertExceptionExpires != target.alertExceptionExpires &&
                alertExceptionExpires > now) {
            // If we got here, the target is subject to a new alert exception window, so we
            // should update our timeout to fire at the end of the new window

            target.cancelTimeoutRunnable?.run()
            target.alertExceptionExpires = alertExceptionExpires
            target.cancelTimeoutRunnable = executor.executeDelayed({
                target.cancelTimeoutRunnable = null
                target.shouldFilter = true
                filter.invalidateList("updateAlertException: ${entry.logKey}")
            }, alertExceptionExpires - now)
        }
    }

    private fun cancelExceptionTimeout(target: TrackedSmartspaceTarget) {
        target.cancelTimeoutRunnable?.run()
        target.cancelTimeoutRunnable = null
        target.alertExceptionExpires = 0
    }

    private fun recordStatusBarState(newState: Int) {
        val wasOnLockscreen = isOnLockscreen
        isOnLockscreen = newState == StatusBarState.KEYGUARD

        if (isOnLockscreen != wasOnLockscreen) {
            filter.invalidateList("recordStatusBarState: " + StatusBarState.toString(newState))
            // No need to call notificationEntryManager.updateNotifications; something else already
            // does it for us when the keyguard state changes
        }
    }

    private fun hasRecentlyAlerted(entry: NotificationEntry): Boolean {
        return clock.currentTimeMillis() - entry.ranking.lastAudiblyAlertedMillis <= ALERT_WINDOW
    }
}

private class TrackedSmartspaceTarget(
    val key: String
) {
    var cancelTimeoutRunnable: Runnable? = null
    var alertExceptionExpires: Long = 0
    var shouldFilter = false
}

private val ALERT_WINDOW = SECONDS.toMillis(30)
