package net.taehui.twilight

import com.fasterxml.jackson.core.JacksonException
import com.ibm.icu.text.CharsetDetector
import net.taehui.twilight.awilight.DefaultCompute
import net.taehui.twilight.note.BaseNote
import net.taehui.twilight.system.Configure
import java.io.ByteArrayInputStream
import java.util.*
import kotlin.math.max


abstract class BaseCompiler(val noteFileData: ByteArray, val format: String) : Logger {
    val notes = mutableListOf<BaseNote>()
    val waitBPMMap = TreeMap<Double, Double>()
    val waitStopMap = TreeMap<Double, Double>()
    val bpmValues = TreeMap<Double, Double>()
    val positionStandNoteCountMap = TreeMap<Double, Int>()
    val waitMultiplierMap = TreeMap<Double, Double>(Comparator.reverseOrder())
    val waitValues = TreeMap<Double, Double>()
    lateinit var valueComponent: Component
    var highestPosition = 0.0

    private fun getWaitValue(waitPosition: Double): Double {
        return waitValues[waitPosition]!!
    }

    fun onCompiled(targetComputing: Computing) {
        if (bpmValues.isNotEmpty()) {
            val bpmWaitMap = mutableMapOf<Double, Double>()
            var lastBPM = targetComputing.levyingBPM
            var lastBPMPosition = 0.0
            bpmValues.forEach {
                if (it.key <= highestPosition) {
                    bpmWaitMap[lastBPM] = bpmWaitMap.getOrDefault(
                        lastBPM,
                        0.0
                    ) + waitValues[it.key]!! - waitValues[lastBPMPosition]!!
                    lastBPMPosition = it.key
                    lastBPM = it.value
                }
            }
            bpmWaitMap[lastBPM] = bpmWaitMap.getOrDefault(
                lastBPM,
                0.0
            ) + waitValues[highestPosition]!! - waitValues[lastBPMPosition]!!
            val longestWait = bpmWaitMap.values.max()
            val longestBPMs = bpmWaitMap.entries
                .filter { it.value == longestWait }
                .map { it.key }
            if (longestBPMs.isNotEmpty()) {
                val validBPMs = longestBPMs.filter { it > 0.5 && it < 65536 }
                if (validBPMs.isNotEmpty()) {
                    targetComputing.bpm = validBPMs.min()
                } else {
                    val validBPMWaitValues: List<Map.Entry<Double, Double>> = bpmWaitMap.entries.filter {
                        it.key > 0.5 && it.key < 65536
                    }
                    if (validBPMWaitValues.isNotEmpty()) {
                        val validLongestWait = validBPMWaitValues.maxOfOrNull { it.value }
                        targetComputing.bpm = validBPMWaitValues.minBy { it.value == validLongestWait }.key
                    } else {
                        val bpm = longestBPMs.max()
                        if (bpm % 100001 == 0.0) {
                            targetComputing.bpm = bpm / 100001
                        } else {
                            targetComputing.bpm = bpm
                        }
                    }
                }
            }
        } else {
            if (targetComputing.levyingBPM % 100001 == 0.0) {
                targetComputing.bpm = targetComputing.levyingBPM / 100001
            } else {
                targetComputing.bpm = targetComputing.levyingBPM
            }
        }
        targetComputing.length = getWaitValue(highestPosition)
        if (positionStandNoteCountMap.isNotEmpty()) {

            val positionStandNoteCounts = mutableListOf<Pair<Double, Int>>()
            positionStandNoteCounts.addAll(positionStandNoteCountMap.toList())
            var lowestPosition = 0
            var lowestWait = getWaitValue(positionStandNoteCounts[lowestPosition].first)
            var lowestCount = positionStandNoteCounts[lowestPosition].second
            var highestInputCount = lowestCount
            var i = 1
            while (i < positionStandNoteCounts.size) {
                val (waitPosition, value) = positionStandNoteCounts[i]
                if (getWaitValue(waitPosition) - lowestWait < 1000.0) {
                    highestInputCount += value
                    ++i
                } else {
                    targetComputing.highestInputCount = max(highestInputCount, targetComputing.highestInputCount)
                    highestInputCount -= lowestCount
                    ++lowestPosition
                    lowestWait = getWaitValue(positionStandNoteCounts[lowestPosition].first)
                    lowestCount = positionStandNoteCounts[lowestPosition].second
                }
            }
            targetComputing.highestInputCount = max(highestInputCount, targetComputing.highestInputCount)
        }
    }

    fun onCompiled(defaultComputer: DefaultCompute) {
        defaultComputer.totalNotes = notes.stream()
            .map { if (it.hasStand()) if (it.longWait > 0.0) 2 else 1 else 0 }
            .reduce(0) { o1, t2 -> o1 + t2 }

        defaultComputer.waitBPMMap.putAll(waitBPMMap)
        waitStopMap.forEach { (wait, stop) ->
            val lastBPMs = defaultComputer.waitBPMMap.filter { it.key <= wait }.toList()
            val lastBPM = if (lastBPMs.isNotEmpty()) lastBPMs.last().second else defaultComputer.levyingBPM
            valueComponent.setBPM(lastBPM)
            defaultComputer.waitBPMMap[wait] = 0.0
            defaultComputer.waitBPMMap[wait + valueComponent.millisMeter * stop] = lastBPM
        }
        var lastWait = Double.MAX_VALUE
        waitMultiplierMap.forEach { (wait, multiplier) ->
            val lastBPMs = defaultComputer.waitBPMMap.filter { it.key <= wait }.toList()
            defaultComputer.waitBPMMap[wait] =
                if (lastBPMs.isNotEmpty()) lastBPMs.last().second else defaultComputer.levyingBPM
            defaultComputer.waitBPMMap.keys.filter { wait <= it && it < lastWait }.forEach {
                defaultComputer.waitBPMMap[it] = defaultComputer.waitBPMMap[it]!! * multiplier
            }
            lastWait = wait
        }

        defaultComputer.length = maxOf((notes.filter { it.hasStand() }.maxOfOrNull { it.wait + it.longWait } ?: 0.0),
            defaultComputer.waitAudioNoteMap.stream().max(Comparator.naturalOrder()).orElse(0.0),
            defaultComputer.waitMediaNoteMap.stream().max(Comparator.naturalOrder()).orElse(0.0)
        )

        notes.sort()
        notes.forEach {
            defaultComputer.notes.add(it)
            it.noteID = defaultComputer.notes.size - 1
        }
    }

    abstract fun handleCompile(targetComputing: Computing)

    abstract fun handleCompile(defaultComputer: DefaultCompute)

    companion object {
        fun handleCompile(noteFileData: ByteArray, dataID: Int): Array<Computing> {
            val noteID512 = Utility.getID512(noteFileData)
            val targetComputings = mutableListOf<Computing>()
            ByteArrayInputStream(noteFileData).use {
                val formatComputer = CharsetDetector()
                formatComputer.setText(it)
                val formats = formatComputer.detectAll()
                val format = if (formats[0].confidence >= 87.5) formats[0].name else Configure.db.format
                var targetCompiler: BaseCompiler
                try {
                    val targetComputing = Computing("$noteID512:0")
                    targetCompiler = BMSONCompiler(noteFileData, format)
                    targetCompiler.handleCompile(targetComputing)
                    targetCompiler.onCompiled(targetComputing)
                    targetComputings.add(targetComputing)
                } catch (e: JacksonException) {
                    fun handleBMSCompile(dataID: Int): Computing {
                        val targetComputing = Computing("$noteID512:$dataID")
                        targetCompiler = BMSCompiler(noteFileData, format)
                        targetCompiler.handleCompile(targetComputing)
                        targetCompiler.onCompiled(targetComputing)
                        targetComputings.add(targetComputing)
                        return targetComputing
                    }

                    if (handleBMSCompile(if (dataID == -1) 0 else dataID).inputMode == Component.InputMode.INPUT_MODE_10_2 && dataID == -1) {
                        handleBMSCompile(1)
                    }
                }
                Unit
            }
            return targetComputings.toTypedArray()
        }

        fun handleCompile(defaultComputer: DefaultCompute, noteFileData: ByteArray) {
            ByteArrayInputStream(noteFileData).use {
                val formatComputer = CharsetDetector()
                formatComputer.setText(it)
                val formats = formatComputer.detectAll()
                val format = if (formats[0].confidence > 50) formats[0].name else Configure.db.format
                var targetCompiler: BaseCompiler
                try {
                    targetCompiler = BMSONCompiler(noteFileData, format)
                    targetCompiler.handleCompile(defaultComputer)
                } catch (e: Throwable) {
                    targetCompiler = BMSCompiler(noteFileData, format)
                    targetCompiler.handleCompile(defaultComputer)
                }
                targetCompiler.onCompiled(defaultComputer as Computing)
                targetCompiler.onCompiled(defaultComputer)
            }
        }
    }
}