/*
 * Copyright (C) 2022 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.notifcollection

import android.service.notification.NotificationListenerService.RankingMap
import android.util.ArrayMap
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printCollection
import java.io.PrintWriter

class NotifCollectionInconsistencyTracker(val logger: NotifCollectionLogger) {
    fun attach(
        collectedKeySetAccessor: () -> Set<String>,
        coalescedKeySetAccessor: () -> Set<String>,
    ) {
        if (attached) {
            throw RuntimeException("attach() called twice")
        }
        attached = true
        this.collectedKeySetAccessor = collectedKeySetAccessor
        this.coalescedKeySetAccessor = coalescedKeySetAccessor
    }

    fun logNewMissingNotifications(rankingMap: RankingMap) {
        val currentCollectedKeys = collectedKeySetAccessor()
        val currentCoalescedKeys = coalescedKeySetAccessor()
        val newMissingNotifications = rankingMap.orderedKeys.asSequence()
            .filter { it !in currentCollectedKeys }
            .filter { it !in currentCoalescedKeys }
            .toSet()
        maybeLogMissingNotifications(missingNotifications, newMissingNotifications)
        missingNotifications = newMissingNotifications
    }

    @VisibleForTesting
    fun maybeLogMissingNotifications(
        oldMissingKeys: Set<String>,
        newMissingKeys: Set<String>,
    ) {
        if (oldMissingKeys.isEmpty() && newMissingKeys.isEmpty()) return
        if (oldMissingKeys == newMissingKeys) return
        (oldMissingKeys - newMissingKeys).sorted().let { justFound ->
            if (justFound.isNotEmpty()) {
                logger.logFoundNotifications(justFound, newMissingKeys.size)
            }
        }
        (newMissingKeys - oldMissingKeys).sorted().let { goneMissing ->
            if (goneMissing.isNotEmpty()) {
                logger.logMissingNotifications(goneMissing, newMissingKeys.size)
            }
        }
    }

    fun logNewInconsistentRankings(
        currentEntriesWithoutRankings: ArrayMap<String, NotificationEntry>?,
        rankingMap: RankingMap,
    ) {
        maybeLogInconsistentRankings(
            notificationsWithoutRankings,
            currentEntriesWithoutRankings ?: emptyMap(),
            rankingMap
        )
        notificationsWithoutRankings = currentEntriesWithoutRankings?.keys ?: emptySet()
    }

    @VisibleForTesting
    fun maybeLogInconsistentRankings(
        oldKeysWithoutRankings: Set<String>,
        newEntriesWithoutRankings: Map<String, NotificationEntry>,
        rankingMap: RankingMap,
    ) {
        if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings.isEmpty()) return
        if (oldKeysWithoutRankings == newEntriesWithoutRankings.keys) return
        val newlyConsistent: List<String> = oldKeysWithoutRankings
            .mapNotNull { key ->
                key.takeIf { key !in newEntriesWithoutRankings }
                    .takeIf { key in rankingMap.orderedKeys }
            }.sorted()
        if (newlyConsistent.isNotEmpty()) {
            val totalInconsistent: Int = newEntriesWithoutRankings.size
            logger.logRecoveredRankings(newlyConsistent, totalInconsistent)
        }
        val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings
            .mapNotNull { (key, entry) ->
                entry.takeIf { key !in oldKeysWithoutRankings }
            }.sortedBy { it.key }
        if (newlyInconsistent.isNotEmpty()) {
            val totalInconsistent: Int = newEntriesWithoutRankings.size
            logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap)
        }
    }

    fun dump(pw: PrintWriter) = pw.asIndenting().run {
        printCollection("notificationsWithoutRankings", notificationsWithoutRankings)
        printCollection("missingNotifications", missingNotifications)
    }

    private var attached: Boolean = false
    private lateinit var collectedKeySetAccessor: (() -> Set<String>)
    private lateinit var coalescedKeySetAccessor: (() -> Set<String>)
    private var notificationsWithoutRankings = emptySet<String>()
    private var missingNotifications = emptySet<String>()
}
