package net.taehui.twilight

import com.fasterxml.jackson.databind.ObjectMapper
import net.taehui.twilight.awilight.DefaultCompute
import net.taehui.twilight.note.InputNote
import net.taehui.twilight.note.LongNote
import net.taehui.twilight.note.MeterNote
import net.taehui.twilight.note.TrapNote
import org.apache.commons.lang3.ArrayUtils
import java.nio.charset.Charset
import java.util.*
import kotlin.math.abs

class BMSONCompiler(noteFileData: ByteArray, format: String) : BaseCompiler(noteFileData, format) {
    private val bmsonPositionLogicalYMap = TreeMap<Long, Double>()
    private val bmsonPositionBPMMap = TreeMap<Long, Double>()
    private lateinit var text: JSON.Bmson
    private var res = 0L

    override fun handleCompile(targetComputing: Computing) {
        text = ObjectMapper().readValue(String(noteFileData, Charset.forName(format)), JSON.Bmson::class.java)
        val title = text.info.title
        val titleAssister0 = text.info.subtitle
        val titleAssister1 = text.info.chart_name
        targetComputing.title =
            "$title${if (titleAssister0.isEmpty() || title.endsWith(titleAssister0)) "" else " $titleAssister0"}${
                if (titleAssister1.isEmpty() || title.endsWith(
                        titleAssister1
                    )
                ) "" else " $titleAssister1"
            }"
        targetComputing.artist =
            mutableListOf(text.info.artist, *text.info.subartists.toTypedArray()).joinToString(" / ")
        targetComputing.genre = text.info.genre
        val levelTextValue = abs(text.info.level)
        if (levelTextValue < 100) {
            targetComputing.levelText = "LV. $levelTextValue"
        } else {
            val levelText = levelTextValue.toString()
            targetComputing.levelText =
                "LV. ${levelText.substring(0.coerceAtLeast(levelText.length - 2), levelText.length)}"
        }
        val longNoteVariety = text.info.ln_type
        targetComputing.isAutoLongNote = longNoteVariety != 2L && longNoteVariety != 3L
        when (text.info.mode_hint) {
            "generic-4keys" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_4
            "generic-5keys", "popn-5k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_5
            "generic-6keys" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_6
            "generic-8keys" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_8
            "generic-9keys", "popn-9k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_9
            "generic-10keys" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_10
            "beat-5k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_5_1
            "beat-7k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_7_1
            "beat-10k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_10_2
            "beat-14k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_14_2
            "keyboard-24k" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_24_2
            "keyboard-24k-double" -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_48_4
            else -> targetComputing.inputMode = Component.InputMode.INPUT_MODE_7
        }
        targetComputing.judgmentStage = 70 / (0.03 * text.info.judge_rank + 7)
        targetComputing.hitPointsValue = text.info.total / 10000
        val bmsonPositionSet = TreeSet<Long>()
        bmsonPositionSet.add(0L)
        var audioCount = 0
        for (audioChannel in text.sound_channels) {
            if (audioChannel.name.isNotEmpty()) {
                ++audioCount
            }
            for (note in audioChannel.notes) {
                var bmsonPosition = note.y
                val input = getBMSONInput(note.x.toInt(), targetComputing.inputMode)
                val isInput = input > 0
                val isLongInput = note.l > 0L
                bmsonPositionSet.add(bmsonPosition)
                if (isInput) {
                    positionStandNoteCountMap[bmsonPosition.toDouble()] =
                        positionStandNoteCountMap.getOrDefault(bmsonPosition.toDouble(), 0) + 1
                    val isAutoableInput =
                        ArrayUtils.indexOf(Component.AUTOABLE_INPUTS[targetComputing.inputMode.value], input) != -1
                    targetComputing.totalNotes += 1
                    if (isAutoableInput) {
                        targetComputing.autoableNotes += 1
                    }
                    if (isLongInput) {
                        targetComputing.longNotes += 1
                    }
                }
                if (isLongInput) {
                    bmsonPosition += note.l
                    bmsonPositionSet.add(bmsonPosition)
                }
                highestPosition = highestPosition.coerceAtLeast(bmsonPosition.toDouble())
            }
        }
        if (targetComputing.totalNotes == 0) {
            targetComputing.inputMode = Component.InputMode.INPUT_MODE_5_1
        }
        for (trapChannel in text.sound_channels) {
            for (note in trapChannel.notes) {
                val bmsonPosition = note.y
                val input = getBMSONInput(note.x.toInt(), targetComputing.inputMode)
                val isInput = input > 0
                bmsonPositionSet.add(bmsonPosition)
                if (isInput) {
                    ++targetComputing.trapNotes
                }
                highestPosition = highestPosition.coerceAtLeast(bmsonPosition.toDouble())
            }
        }
        text.bga.bga_events.forEach {
            bmsonPositionSet.add(it.y)
        }
        text.bga.layer_events.forEach {
            bmsonPositionSet.add(it.y)
        }
        text.bga.poor_events.forEach {
            bmsonPositionSet.add(it.y)
        }
        text.lines.forEach {
            val bmsonPosition = it.y
            bmsonPositionSet.add(bmsonPosition)
            highestPosition = highestPosition.coerceAtLeast(bmsonPosition.toDouble())
        }
        text.bpm_events.forEach {
            val bmsonPosition = it.y
            bpmValues[bmsonPosition.toDouble()] = it.bpm
            bmsonPositionSet.add(bmsonPosition)
        }
        val bmsonPositionStopMap = TreeMap<Long, Long>()
        text.stop_events.forEach {
            val bmsonPosition = it.y
            bmsonPositionStopMap[bmsonPosition] = bmsonPositionStopMap.getOrDefault(bmsonPosition, 0L) + it.duration
            bmsonPositionSet.add(bmsonPosition)
        }
        targetComputing.levyingBPM = text.info.init_bpm
        valueComponent = Component(targetComputing.levyingBPM)
        res = text.info.resolution * 4L
        var lastBMSONPosition = 0L
        var lastWait = 0.0
        var lastBPM = targetComputing.levyingBPM
        for (bmsonPosition in bmsonPositionSet) {
            bmsonPositionLogicalYMap[bmsonPosition] =
                bmsonPosition.toDouble() * -valueComponent.logicalYMeter / res.toDouble()
            lastWait += valueComponent.millisMeter * (bmsonPosition - lastBMSONPosition).toDouble() / res.toDouble()
            bpmValues[bmsonPosition.toDouble()]?.let {
                lastBPM = it
                valueComponent.setBPM(it)
            }
            waitValues[bmsonPosition.toDouble()] = lastWait
            lastBMSONPosition = bmsonPosition
            bmsonPositionStopMap[bmsonPosition]?.let {
                lastWait += valueComponent.millisMeter * it.toDouble() / res.toDouble()
                bmsonPositionBPMMap[bmsonPosition] = lastBPM
            }
        }
        targetComputing.isBanned = audioCount < 2 || targetComputing.totalNotes == 0
        targetComputing.noteVariety = Component.NoteVariety.BMSON
    }

    override fun handleCompile(defaultComputer: DefaultCompute) {
        handleCompile(defaultComputer as Computing)
        text.lines.forEach {
            val bmsonPosition = it.y
            val logicalY = valueComponent.levyingHeight + bmsonPositionLogicalYMap[bmsonPosition]!!
            val wait = waitValues[bmsonPosition.toDouble()]!!
            notes.add(MeterNote(logicalY, wait))
        }
        text.sound_channels.forEach {
            it.notes.sortedBy { note -> note.y }.forEach { note ->
                val bmsonPosition = note.y
                val logicalY = valueComponent.levyingHeight + bmsonPositionLogicalYMap[bmsonPosition]!!
                val wait = waitValues[bmsonPosition.toDouble()]!!
                val input = getBMSONInput(note.x.toInt(), defaultComputer.inputMode)
                if (input > 0) {
                    if (note.l > 0L) {
                        notes.add(
                            LongNote(
                                logicalY,
                                wait,
                                input,
                                waitValues[(bmsonPosition + note.l).toDouble()]!! - wait,
                                logicalY - (valueComponent.levyingHeight + bmsonPositionLogicalYMap[bmsonPosition + note.l]!!)
                            )
                        )
                    } else {
                        notes.add(InputNote(logicalY, wait, input))
                    }
                }
            }
        }
        text.mine_channels.forEach {
            it.notes.sortedBy { note -> note.y }.forEach { note ->
                val input = getBMSONInput(note.x.toInt(), defaultComputer.inputMode)
                if (input > 0) {
                    val bmsonPosition = note.y
                    val logicalY = valueComponent.levyingHeight + bmsonPositionLogicalYMap[bmsonPosition]!!
                    val wait = waitValues[bmsonPosition.toDouble()]!!
                    notes.add(TrapNote(logicalY, wait, input))
                }
            }
        }
        bpmValues.forEach {
            waitBPMMap[waitValues[it.key]!!] = it.value
        }
        text.stop_events.forEach {
            val bmsonPosition = it.y
            bmsonPositionBPMMap[bmsonPosition]?.let { bpm ->
                val wait = waitValues[bmsonPosition.toDouble()]!!
                valueComponent.setBPM(bpm)
                waitStopMap[wait] = 0.0
                waitStopMap[wait + valueComponent.millisMeter * it.duration.toDouble() / res.toDouble()] = bpm
            }
        }
    }

    private fun getBMSONInput(x: Int, inputMode: Component.InputMode): Int {
        when (inputMode) {
            Component.InputMode.INPUT_MODE_4 -> {
                when (x) {
                    1, 2, 3, 4 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_5 -> {
                when (x) {
                    1, 2, 3, 4, 5 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_6 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_7 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6, 7 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_8 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6, 7, 8 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_9 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6, 7, 8, 9 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_10 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_5_1 -> {
                when (x) {
                    8 -> {
                        return 1
                    }

                    1, 2, 3, 4, 5 -> {
                        return x + 1
                    }
                }
            }

            Component.InputMode.INPUT_MODE_7_1 -> {
                when (x) {
                    8 -> {
                        return 1
                    }

                    1, 2, 3, 4, 5, 6, 7 -> {
                        return x + 1
                    }
                }
            }

            Component.InputMode.INPUT_MODE_10_2 -> {
                when (x) {
                    8 -> {
                        return 1
                    }

                    16 -> {
                        return 12
                    }

                    1, 2, 3, 4, 5 -> {
                        return x + 1
                    }

                    9, 10, 11, 12, 13 -> {
                        return x - 2
                    }
                }
            }

            Component.InputMode.INPUT_MODE_14_2 -> {
                when (x) {
                    8 -> {
                        return 1
                    }

                    1, 2, 3, 4, 5, 6, 7 -> {
                        return x + 1
                    }

                    9, 10, 11, 12, 13, 14, 15, 16 -> {
                        return x
                    }
                }
            }

            Component.InputMode.INPUT_MODE_24_2 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 -> {
                        return x + 1
                    }

                    25 -> {
                        return 1
                    }

                    26 -> {
                        return 26
                    }
                }
            }

            Component.InputMode.INPUT_MODE_48_4 -> {
                when (x) {
                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 -> {
                        return x + 2
                    }

                    27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50 -> {
                        return x
                    }

                    25 -> {
                        return 1
                    }

                    26 -> {
                        return 2
                    }

                    51 -> {
                        return 51
                    }

                    52 -> {
                        return 52
                    }
                }
            }
        }
        return 0
    }
}