/*
 * Copyright (C) 2020 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

import android.app.Notification
import android.content.Context
import android.content.pm.LauncherApps
import android.graphics.drawable.AnimatedImageDrawable
import android.os.Handler
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.NotificationListenerService.RankingMap
import com.android.internal.statusbar.NotificationVisibility
import com.android.internal.widget.ConversationLayout
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLayout
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationContentView
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
import com.android.systemui.util.children
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject

/** Populates additional information in conversation notifications */
class ConversationNotificationProcessor @Inject constructor(
    private val launcherApps: LauncherApps,
    private val conversationNotificationManager: ConversationNotificationManager
) {
    fun processNotification(entry: NotificationEntry, recoveredBuilder: Notification.Builder) {
        val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return
        messagingStyle.conversationType =
                if (entry.ranking.channel.isImportantConversation)
                    Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
                else
                    Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL
        entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->
            messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
            shortcutInfo.label?.let { label ->
                messagingStyle.conversationTitle = label
            }
        }
        messagingStyle.unreadMessageCount =
                conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
    }
}

/**
 * Tracks state related to animated images inside of notifications. Ex: starting and stopping
 * animations to conserve CPU and memory.
 */
@SysUISingleton
class AnimatedImageNotificationManager @Inject constructor(
    private val notificationEntryManager: NotificationEntryManager,
    private val headsUpManager: HeadsUpManager,
    private val statusBarStateController: StatusBarStateController
) {

    private var isStatusBarExpanded = false

    /** Begins listening to state changes and updating animations accordingly. */
    fun bind() {
        headsUpManager.addListener(object : OnHeadsUpChangedListener {
            override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
                entry.row?.let { row ->
                    updateAnimatedImageDrawables(row, animating = isHeadsUp || isStatusBarExpanded)
                }
            }
        })
        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
            override fun onExpandedChanged(isExpanded: Boolean) {
                isStatusBarExpanded = isExpanded
                notificationEntryManager.activeNotificationsForCurrentUser.forEach { entry ->
                    entry.row?.let { row ->
                        updateAnimatedImageDrawables(row, animating = isExpanded || row.isHeadsUp)
                    }
                }
            }
        })
        notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
            override fun onEntryInflated(entry: NotificationEntry) {
                entry.row?.let { row ->
                    updateAnimatedImageDrawables(
                            row,
                            animating = isStatusBarExpanded || row.isHeadsUp)
                }
            }
            override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry)
        })
    }

    private fun updateAnimatedImageDrawables(row: ExpandableNotificationRow, animating: Boolean) =
            (row.layouts?.asSequence() ?: emptySequence())
                    .flatMap { layout -> layout.allViews.asSequence() }
                    .flatMap { view ->
                        (view as? ConversationLayout)?.messagingGroups?.asSequence()
                                ?: (view as? MessagingLayout)?.messagingGroups?.asSequence()
                                ?: emptySequence()
                    }
                    .flatMap { messagingGroup -> messagingGroup.messageContainer.children }
                    .mapNotNull { view ->
                        (view as? MessagingImageMessage)
                                ?.let { imageMessage ->
                                    imageMessage.drawable as? AnimatedImageDrawable
                                }
                    }
                    .forEach { animatedImageDrawable ->
                        if (animating) animatedImageDrawable.start()
                        else animatedImageDrawable.stop()
                    }
}

/**
 * Tracks state related to conversation notifications, and updates the UI of existing notifications
 * when necessary.
 */
@SysUISingleton
class ConversationNotificationManager @Inject constructor(
    private val notificationEntryManager: NotificationEntryManager,
    private val notificationGroupManager: NotificationGroupManagerLegacy,
    private val context: Context,
    @Main private val mainHandler: Handler
) {
    // Need this state to be thread safe, since it's accessed from the ui thread
    // (NotificationEntryListener) and a bg thread (NotificationContentInflater)
    private val states = ConcurrentHashMap<String, ConversationState>()

    private var notifPanelCollapsed = true

    init {
        notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
            override fun onNotificationRankingUpdated(rankingMap: RankingMap) {
                fun getLayouts(view: NotificationContentView) =
                        sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild)
                val ranking = Ranking()
                val activeConversationEntries = states.keys.asSequence()
                        .mapNotNull { notificationEntryManager.getActiveNotificationUnfiltered(it) }
                for (entry in activeConversationEntries) {
                    if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) {
                        val important = ranking.channel.isImportantConversation
                        var changed = false
                        entry.row?.layouts?.asSequence()
                                ?.flatMap(::getLayouts)
                                ?.mapNotNull { it as? ConversationLayout }
                                ?.filterNot { it.isImportantConversation == important }
                                ?.forEach { layout ->
                                    changed = true
                                    if (important && entry.isMarkedForUserTriggeredMovement) {
                                        // delay this so that it doesn't animate in until after
                                        // the notif has been moved in the shade
                                        mainHandler.postDelayed(
                                                {
                                                    layout.setIsImportantConversation(
                                                            important,
                                                            true)
                                                },
                                                IMPORTANCE_ANIMATION_DELAY.toLong())
                                    } else {
                                        layout.setIsImportantConversation(important, false)
                                    }
                                }
                        if (changed) {
                            notificationGroupManager.updateIsolation(entry)
                        }
                    }
                }
            }

            override fun onEntryInflated(entry: NotificationEntry) {
                if (!entry.ranking.isConversation) {
                    return
                }
                fun updateCount(isExpanded: Boolean) {
                    if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded)) {
                        resetCount(entry.key)
                        entry.row?.let(::resetBadgeUi)
                    }
                }
                entry.row?.setOnExpansionChangedListener { isExpanded ->
                    if (entry.row?.isShown == true && isExpanded) {
                        entry.row.performOnIntrinsicHeightReached {
                            updateCount(isExpanded)
                        }
                    } else {
                        updateCount(isExpanded)
                    }
                }
                updateCount(entry.row?.isExpanded == true)
            }

            override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry)

            override fun onEntryRemoved(
                entry: NotificationEntry,
                visibility: NotificationVisibility?,
                removedByUser: Boolean,
                reason: Int
            ) = removeTrackedEntry(entry)
        })
    }

    private fun ConversationState.shouldIncrementUnread(newBuilder: Notification.Builder) =
            if (notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) {
                false
            } else {
                val oldBuilder = Notification.Builder.recoverBuilder(context, notification)
                Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder)
            }

    fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int =
            states.compute(entry.key) { _, state ->
                val newCount = state?.run {
                    if (shouldIncrementUnread(recoveredBuilder)) unreadCount + 1 else unreadCount
                } ?: 1
                ConversationState(newCount, entry.sbn.notification)
            }!!.unreadCount

    fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) {
        notifPanelCollapsed = isCollapsed
        if (isCollapsed) return

        // When the notification panel is expanded, reset the counters of any expanded
        // conversations
        val expanded = states
                .asSequence()
                .mapNotNull { (key, _) ->
                    notificationEntryManager.getActiveNotificationUnfiltered(key)
                            ?.let { entry ->
                                if (entry.row?.isExpanded == true) key to entry
                                else null
                            }
                }
                .toMap()
        states.replaceAll { key, state ->
            if (expanded.contains(key)) state.copy(unreadCount = 0)
            else state
        }
        // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the
        // lambda if threads are in contention.
        expanded.values.asSequence().mapNotNull { it.row }.forEach(::resetBadgeUi)
    }

    private fun resetCount(key: String) {
        states.compute(key) { _, state -> state?.copy(unreadCount = 0) }
    }

    private fun removeTrackedEntry(entry: NotificationEntry) {
        states.remove(entry.key)
    }

    private fun resetBadgeUi(row: ExpandableNotificationRow): Unit =
            (row.layouts?.asSequence() ?: emptySequence())
                    .flatMap { layout -> layout.allViews.asSequence() }
                    .mapNotNull { view -> view as? ConversationLayout }
                    .forEach { convoLayout -> convoLayout.setUnreadCount(0) }

    private data class ConversationState(val unreadCount: Int, val notification: Notification)

    companion object {
        private const val IMPORTANCE_ANIMATION_DELAY =
                StackStateAnimator.ANIMATION_DURATION_STANDARD +
                StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE +
                100
    }
}
