package net.taehui.twilight.system

import com.fasterxml.jackson.databind.ObjectMapper
import net.taehui.twilight.JSON
import net.taehui.twilight.Logger
import net.taehui.twilight.TwilightComponent
import net.taehui.twilight.Utility
import org.apache.commons.io.FilenameUtils
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.jsoup.Jsoup
import java.io.IOException
import java.nio.file.Files
import java.util.TreeSet
import java.util.concurrent.atomic.AtomicInteger

object AbilitySystem : Logger {
    class DefaultAbility {
        var abilityFiles = emptyArray<AbilityFile>()
        var abilityWww = emptyArray<String>()
        var abilityMap = emptyMap<String, Double>()

        class AbilityFile {
            var fileName = ""
            var abilityClassVariety = AbilityClassSystem.AbilityClassVariety.ETC
        }
    }

    class AbilityMap<T> {
        val noteID128Map = mutableMapOf<String, T>()
        val noteID256Map = mutableMapOf<String, T>()

        fun wipe() {
            noteID128Map.clear()
            noteID256Map.clear()
        }

        fun getValue(noteID128: String, noteID256: String, defaultValue: T): T {
            return noteID128Map.getOrDefault(noteID128, noteID256Map.getOrDefault(noteID256, defaultValue))
        }
    }

    private val noteIDAbilityMap = mapOf<AbilityClassSystem.AbilityClassVariety, AbilityMap<Double>>(
        Pair(AbilityClassSystem.AbilityClassVariety.ETC, AbilityMap()),
        Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K, AbilityMap()),
        Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K, AbilityMap()),
        Pair(AbilityClassSystem.AbilityClassVariety.INPUT_MODE_9K, AbilityMap())
    )
    private val noteIDAbilityIDsMap = AbilityMap<MutableCollection<String>>()
    private val noteIDAbilityNamesMap = AbilityMap<MutableCollection<String>>()
    private val abilityNameAbilityIDsMap = mutableMapOf<String, MutableCollection<String>>()
    private val abilityNameAbilityIDNoteIDCountMap = mutableMapOf<String, MutableMap<String, AtomicInteger>>()
    var isLoading = false

    fun loadAbility() {
        isLoading = true
        logFuture {
            val jm = ObjectMapper()
            val defaultAbility = jm.readValue(
                TwilightComponent.ABILITY_ENTRY_PATH.resolve("Default.json").toFile(),
                DefaultAbility::class.java
            )
            if (Configure.mode.getAbility) {
                defaultAbility.abilityWww.forEach { target ->
                    try {
                        val o = Jsoup.connect(target).get()
                        var dataValue = o.selectXpath("/html/head/meta[@name='bmstable']")
                        if (dataValue.isEmpty()) {
                            dataValue = o.selectXpath("/html/body/meta[@name='bmstable']")
                        }
                        if (dataValue.isEmpty()) {
                            dataValue = o.selectXpath("/html/head/body/meta[@name='bmstable']")
                        }
                        if (!dataValue.isEmpty()) {
                            val www = dataValue[0].attr("content")
                            HttpClients.createDefault().use {
                                fun doModifyDataValue(target: String, dataValue: String): String {
                                    return if (!Utility.isValidWww(dataValue)) {
                                        if (target.substring(target.lastIndexOf('/'))
                                                .contains(".") || target.endsWith("/")
                                        ) {
                                            "$target/../$dataValue"
                                        } else {
                                            "$target/$dataValue"
                                        }
                                    } else {
                                        dataValue
                                    }
                                }

                                val text = it.execute(
                                    HttpGet(doModifyDataValue(target, www)),
                                    BasicHttpClientResponseHandler()
                                )
                                val abilityTable = text.byteInputStream().use { src ->
                                    ObjectMapper().readValue(src, JSON.BMSTable::class.java)
                                }
                                Files.writeString(
                                    TwilightComponent.ABILITY_ENTRY_PATH.resolve("#" + abilityTable.name + ".json"),
                                    text
                                )
                                Files.writeString(
                                    TwilightComponent.ABILITY_ENTRY_PATH.resolve(abilityTable.name + ".json"),
                                    it.execute(
                                        HttpGet(doModifyDataValue(target, abilityTable.data_url)),
                                        BasicHttpClientResponseHandler()
                                    )
                                )
                                logInfo("Saved Ability (${abilityTable.name})")
                            }
                        }
                    } catch (e: IOException) {
                        logFault(e)
                    }
                }
            }
            try {
                val abilityMap = defaultAbility.abilityMap
                noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.ETC]?.wipe()
                noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K]?.wipe()
                noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K]?.wipe()
                noteIDAbilityMap[AbilityClassSystem.AbilityClassVariety.INPUT_MODE_9K]?.wipe()
                noteIDAbilityIDsMap.wipe()
                noteIDAbilityNamesMap.wipe()
                abilityNameAbilityIDsMap.clear()
                abilityNameAbilityIDNoteIDCountMap.clear()
                Files.list(TwilightComponent.ABILITY_ENTRY_PATH).use { abilityFilePaths ->
                    abilityFilePaths.filter { it.fileName.toString().startsWith("#") }.sorted(
                        Comparator.comparingInt { abilityFilePath ->
                            defaultAbility.abilityFiles.indexOfFirst {
                                it.fileName == abilityFilePath.fileName.toString()
                            }
                        }
                    ).forEach { abilityFilePath ->
                        val abilityFileName = abilityFilePath.fileName.toString()
                        val abilityName = FilenameUtils.removeExtension(
                            abilityFileName.substring(abilityFileName.indexOf('#') + 1)
                        )
                        val abilityClassVariety = defaultAbility.abilityFiles.find {
                            it.fileName == abilityFileName
                        }?.abilityClassVariety
                        if (abilityClassVariety != null) {
                            val noteIDAbilityMap = noteIDAbilityMap[abilityClassVariety]
                            val abilityTable = jm.readValue(abilityFilePath.toFile(), JSON.BMSTable::class.java)
                            val levels =
                                abilityTable.level_order.map { "${abilityTable.symbol}$it" }.toTypedArray()
                            jm.readValue(
                                TwilightComponent.ABILITY_ENTRY_PATH.resolve("$abilityName.json")
                                    .toFile(),
                                Array<JSON.BMSTableData>::class.java
                            ).forEach { abilityTableData ->
                                val abilityID = "${abilityTable.symbol}${abilityTableData.level}"
                                val ability = abilityMap[abilityID] ?: 0.0
                                if (ability > 0.0) {
                                    if (abilityTableData.md5.isNotEmpty()) {
                                        noteIDAbilityMap?.noteID128Map?.putIfAbsent(abilityTableData.md5, ability)
                                    }
                                    if (abilityTableData.sha256.isNotEmpty()) {
                                        noteIDAbilityMap?.noteID256Map?.putIfAbsent(abilityTableData.sha256, ability)
                                    }
                                }
                                if (abilityTableData.md5.isNotEmpty()) {
                                    noteIDAbilityIDsMap.noteID128Map.computeIfAbsent(
                                        abilityTableData.md5
                                    ) { mutableListOf() }.add(abilityID)
                                    noteIDAbilityNamesMap.noteID128Map.computeIfAbsent(
                                        abilityTableData.md5
                                    ) { mutableListOf() }.add(abilityName)
                                }
                                if (abilityTableData.sha256.isNotEmpty()) {
                                    noteIDAbilityIDsMap.noteID256Map.computeIfAbsent(
                                        abilityTableData.sha256
                                    ) { mutableListOf() }.add(abilityID)
                                    noteIDAbilityNamesMap.noteID256Map.computeIfAbsent(
                                        abilityTableData.md5
                                    ) { mutableListOf() }.add(abilityName)
                                }
                                abilityNameAbilityIDsMap.computeIfAbsent(abilityName) {
                                    if (levels.isEmpty()) mutableSetOf() else TreeSet { o1, o2 ->
                                        levels.indexOf(o1).compareTo(levels.indexOf(o2))
                                    }
                                }.add(abilityID)
                                abilityNameAbilityIDNoteIDCountMap.computeIfAbsent(abilityName) {
                                    mutableMapOf()
                                }.computeIfAbsent(abilityID) {
                                    AtomicInteger()
                                }.incrementAndGet()
                            }
                            logInfo("Loaded Ability (${abilityTable.name})")
                        }
                    }
                }
            } catch (e: IOException) {
                logFault(e)
            }
            try {
                DB.learnAbility()
                logInfo("Learned Ability")
            } catch (e: Throwable) {
                logFault(e)
            }
        }.thenAccept {
            isLoading = false
        }
    }

    fun getAbility(
        abilityClassVariety: AbilityClassSystem.AbilityClassVariety,
        noteID128: String,
        noteID256: String
    ): Double {
        return noteIDAbilityMap[abilityClassVariety]?.getValue(noteID128, noteID256, 0.0) ?: 0.0
    }

    fun getAbilityIDs(
        noteID128: String,
        noteID256: String
    ): Collection<String> {
        return noteIDAbilityIDsMap.getValue(noteID128, noteID256, mutableListOf())
    }

    fun getAbilityNames(
        noteID128: String,
        noteID256: String
    ): Collection<String> {
        return noteIDAbilityNamesMap.getValue(noteID128, noteID256, mutableListOf())
    }

    fun getAbilityIDs(
        abilityName: String
    ): Collection<String> {
        return abilityNameAbilityIDsMap.getOrDefault(abilityName, emptyList())
    }

    fun getAbilityNames(): Collection<String> {
        return abilityNameAbilityIDsMap.keys
    }

    fun getNoteIDCount(abilityName: String, abilityID: String): Int {
        return if (abilityID == "*") abilityNameAbilityIDNoteIDCountMap.getOrDefault(
            abilityName,
            emptyMap()
        ).values.sumOf { it.get() } else abilityNameAbilityIDNoteIDCountMap.getOrDefault(abilityName, emptyMap())
            .getOrDefault(abilityID, AtomicInteger()).get()
    }
}