package com.topjohnwu.magisk.ui.deny

import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.*
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import android.os.Build.VERSION.SDK_INT
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.ktx.getLabel
import java.util.*

class CmdlineListItem(line: String) {
    val packageName: String
    val process: String

    init {
        val split = line.split(Regex("\\|"), 2)
        packageName = split[0]
        process = split.getOrElse(1) { packageName }
    }
}

const val ISOLATED_MAGIC = "isolated"

@SuppressLint("InlinedApi")
class AppProcessInfo(
    info: ApplicationInfo,
    pm: PackageManager,
    denyList: List<CmdlineListItem>
) : ApplicationInfo(info), Comparable<AppProcessInfo> {

    private val denyList = denyList.filter {
        it.packageName == packageName || it.packageName == ISOLATED_MAGIC
    }

    val label = getLabel(pm)
    val iconImage: Drawable = loadIcon(pm)
    val processes = fetchProcesses(pm)

    override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)

    private fun createProcess(name: String, pkg: String = packageName) =
        ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })

    private fun ComponentInfo.getProcName(): String = processName
        ?: applicationInfo.processName
        ?: applicationInfo.packageName

    private val ServiceInfo.isIsolated get() = (flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
    private val ServiceInfo.useAppZygote get() = (flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0

    private fun Array<out ComponentInfo>?.toProcessList() =
        this?.map { createProcess(it.getProcName()) }.orEmpty()

    private fun Array<ServiceInfo>?.toProcessList() = this?.map {
        if (it.isIsolated) {
            if (it.useAppZygote) {
                val proc = processName ?: packageName
                createProcess("${proc}_zygote")
            } else {
                val proc = if (SDK_INT >= 29) "${it.getProcName()}:${it.name}" else it.getProcName()
                createProcess(proc, ISOLATED_MAGIC)
            }
        } else {
            createProcess(it.getProcName())
        }
    }.orEmpty()

    private fun fetchProcesses(pm: PackageManager): Collection<ProcessInfo> {
        val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
            GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
        val packageInfo = try {
            pm.getPackageInfo(packageName, flag)
        } catch (e: Exception) {
            // Exceed binder data transfer limit, parse the package locally
            pm.getPackageArchiveInfo(sourceDir, flag) ?: return emptyList()
        }

        val processSet = TreeSet<ProcessInfo>(compareBy{ it.name })
        processSet += packageInfo.activities.toProcessList()
        processSet += packageInfo.services.toProcessList()
        processSet += packageInfo.receivers.toProcessList()
        processSet += packageInfo.providers.toProcessList()
        return processSet
    }

    companion object {
        private val comparator = compareBy<AppProcessInfo>(
            { it.label.lowercase(currentLocale) },
            { it.packageName }
        )
    }
}

data class ProcessInfo(
    val name: String,
    val packageName: String,
    var isEnabled: Boolean
) {
    val isIsolated get() = packageName == ISOLATED_MAGIC
    val isAppZygote get() = name.endsWith("_zygote")
}
