package de.sebse.fuplanner2.utils

//import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.navigation.NavController
import com.beust.klaxon.JsonObject
import de.sebse.fuplanner2.MainActivity
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.*

object Notifications {

    private const val COURSE_UPDATE_NOTIFICATION_ID: Int = 456987
    internal const val COURSE_UPDATE_CHANNEL_ID: String = "COURSE_UPDATE_CHANNEL_ID"
    internal const val COURSE_UPDATE_TYPE_KEY: String = "COURSE_UPDATE_TYPE_KEY"
    internal const val COURSE_UPDATE_DATA_KEY: String = "COURSE_UPDATE_DATA_KEY"
    internal const val COURSE_UPDATE_ENTITY_KEY: String = "COURSE_UPDATE_ENTITY_KEY"

    enum class CourseUpdateType {
        REMOVED, UPDATED, ADDED;
        companion object {
            val values: List<CourseUpdateType> = values().toList()
        }
    }

    enum class CourseUpdateEntity {
        COURSE, ANNOUNCEMENT, ASSIGNMENT, GRADE, RESOURCE, EVENT;
        companion object {
            val values: List<CourseUpdateEntity> = values().toList()
        }
    }

    fun init(actCtx: Context) {
        createNotificationChannel(actCtx, COURSE_UPDATE_CHANNEL_ID, R.string.channel_name, R.string.channel_description)
    }

    private fun createNotificationChannel(
        actCtx: Context,
        channelId: String,
        @StringRes channelName: Int,
        @StringRes channelDescription: Int
    ) {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = actCtx.getString(channelName)
            val descriptionText = actCtx.getString(channelDescription)
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(channelId, name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            (actCtx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
                .createNotificationChannel(channel)
        }
    }

    fun <T: Updatable> courseUpdates(updates: UpdateResult<T>, database: AppDatabase, actCtx: Context) {
        val newNotifications = listOf(
            CourseUpdateType.REMOVED to updates.removed,
            CourseUpdateType.ADDED to updates.added,
            CourseUpdateType.UPDATED to updates.updated
        ).map { pair ->
            pair.second.map {
                Notification(
                    null,
                    System.currentTimeMillis(),
                    false,
                    COURSE_UPDATE_CHANNEL_ID,
                    JsonObject().apply {
                        put(COURSE_UPDATE_TYPE_KEY, pair.first.ordinal)
                        put(COURSE_UPDATE_DATA_KEY, it.getData())
                        put(COURSE_UPDATE_ENTITY_KEY, it.getNotificationEntityType().ordinal)
                    }.toJsonString()
                )
            }
        }.flatten()
        database.notificationDao().upsert(newNotifications)
        if (newNotifications.isEmpty()) return
        val unread = database.notificationDao().loadAllUnreadByChannelId(COURSE_UPDATE_CHANNEL_ID)
        val pendingIntent: PendingIntent = Intent(actCtx, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            putExtra(MainActivity.EXTRA_OPEN_NOTIFICATIONS, true)
        }.let {
            PendingIntent.getActivity(actCtx, 0, it, 0)
        }

        val notification = NotificationCompat.Builder(actCtx, COURSE_UPDATE_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_logo_mono)
            .setContentTitle(actCtx.getString(R.string.not_course_update_title))
            .setContentText(actCtx.getQuantityString(R.plurals.not_course_update_text, unread.size, unread.size))
            .setStyle(NotificationCompat.InboxStyle().also { inboxStyle ->
                unread.forEach { not ->
                    not.getJsonData()?.let { json ->
                        val updateType =
                            CourseUpdateType.values[json.int(COURSE_UPDATE_TYPE_KEY) ?: 0]
                        val entityType =
                            CourseUpdateEntity.values[json.int(COURSE_UPDATE_ENTITY_KEY) ?: 0]
                        val data = json.obj(COURSE_UPDATE_DATA_KEY) ?: JsonObject()
                        when (entityType) {
                            CourseUpdateEntity.COURSE -> Course
                            CourseUpdateEntity.ANNOUNCEMENT -> Announcement
                            CourseUpdateEntity.ASSIGNMENT -> TODO()
                            CourseUpdateEntity.GRADE -> TODO()
                            CourseUpdateEntity.RESOURCE -> TODO()
                            CourseUpdateEntity.EVENT -> Event
                        }.notificationText(actCtx, updateType, data)
                    }?.let { inboxStyle.addLine(it) }
                }
            })
            .setContentIntent(pendingIntent)
            .build()
        (actCtx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
            .notify(actCtx.packageName, COURSE_UPDATE_NOTIFICATION_ID, notification)
    }
}

data class UpdateResult<T: Updatable>(
    val removed: ArrayList<T> = arrayListOf(),
    val added: ArrayList<T> = arrayListOf(),
    val updated: ArrayList<T> = arrayListOf(),
    val unmodified: ArrayList<T> = arrayListOf()
) {
    fun merge(other: UpdateResult<T>): UpdateResult<T> {
        this.removed.addAll(other.removed)
        this.added.addAll(other.added)
        this.updated.addAll(other.updated)
        this.unmodified.addAll(other.unmodified)
        return this
    }

    val values: List<T>
    get() {
        return this.added + this.updated + this.unmodified
    }
}

fun <S: Updatable> mergeUpdatable(source: UpdateResult<Updatable>, plus: UpdateResult<S>): UpdateResult<Updatable> {
    source.removed.addAll(plus.removed)
    source.added.addAll(plus.added)
    source.updated.addAll(plus.updated)
    source.unmodified.addAll(plus.unmodified)
    return source
}

interface Updatable {
    fun getIdentifier(): String
    fun getHash(): Int
    fun getData(): JsonObject
    fun getNotificationEntityType(): Notifications.CourseUpdateEntity
}

interface UpdatableCompanion {
    fun notificationText(
        actCtx: Context,
        type: Notifications.CourseUpdateType,
        data: JsonObject
    ): CharSequence?
    fun adapterText(
        actCtx: Context,
        type: Notifications.CourseUpdateType,
        data: JsonObject
    ): CharSequence?
    fun adapterCallback(
        actCtx: Context,
        type: Notifications.CourseUpdateType,
        data: JsonObject,
        navController: NavController
    ): () -> Unit
}

fun <T: Updatable> updateResultOf(old: List<T>, new: List<T>): UpdateResult<T> {
    val newIds = new
        .map { it.getIdentifier() to Pair(it.getHash(), it) }
        .toMap()
    val oldIds = old.map { it.getIdentifier() }.toSet()
    val removed: ArrayList<T> = arrayListOf()
    val added: ArrayList<T> = arrayListOf()
    val updated: ArrayList<T> = arrayListOf()
    val unmodified: ArrayList<T> = arrayListOf()
    old.forEach {
        newIds[it.getIdentifier()]?.run {
            if (it.getHash() == first) {
                unmodified.add(second)
            } else {
                updated.add(second)
            }
        } ?: removed.add(it)
    }
    newIds.forEach {
        if (!oldIds.contains(it.key))
            added.add(it.value.second)
    }
    return UpdateResult(removed, added, updated, unmodified)
}

