package mg.dot.feufaro.midi

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.solfa.Transpose
import mg.dot.feufaro.viewmodel.MidiMarkers

class MidiWriterforEXport  constructor(private val fileRepository: FileRepository) {
    private val sequence = MidiSequence(480)
    private val track    = sequence.createTrack()
    private var tick: Long = 0
    private var nextTick: MutableList<MidiPitch> = mutableListOf()
    private val lastPitch : MutableList<Int> = mutableListOf()
    private val useChord : Boolean = true
    fun addNote( voiceNumber: Int, note: Int, velocity: Int, tick: Long) {
        val channel = (voiceNumber -1).coerceIn(0, 3)
        var finalNote = note

        if (voiceNumber == 3 || voiceNumber == 4) {
            finalNote -= 12
        }
        if (lastPitch.size > voiceNumber && lastPitch[voiceNumber] > 0) {
            sequence.addSequence(channel, lastPitch[voiceNumber], tick)
        }
        var finalVelocity = velocity
        var midiNote = finalNote
        if (finalNote <= 0) {
            midiNote = 40
            finalVelocity = 0
        }
        sequence.addSequence(channel, midiNote, tick, 90, finalVelocity)

        while(lastPitch.size <= voiceNumber) {
            lastPitch.add(0)
        }
        lastPitch[voiceNumber] = midiNote
    }
    suspend fun save(filePath: String) = withContext(Dispatchers.Default) {
        sequence.write()
        fileRepository.saveLocalFile(filePath, sequence.out.toByteArray())
    }
    fun addMetaMessage(type: Int, tick: Int, nbData: Int, metaBytes: ByteArray) {
        sequence.addMetaFe(type, tick, nbData, metaBytes)
    }

    fun setTempoMeta(bpm: Float, tickPosition: Int = 0) {
        val mpqn = (60_000_000f / bpm.coerceAtLeast(1f)).toInt()
        val bytes = byteArrayOf(
            (mpqn shr 16 and 0xFF).toByte(),
            (mpqn shr 8 and 0xFF).toByte(),
            (mpqn and 0xFF).toByte()
        )
        addMetaMessage(0x51, tickPosition, 3, bytes)
    }
    fun setProgramChange(voiceNumber: Int, instrumentProgram: Int, tickPosition: Long = 0) {
        val channel = (voiceNumber - 1).coerceIn(0, 3)
        val program = instrumentProgram.coerceIn(0, 127)
        sequence.addSequence(channel, program, tickPosition, 0xC0, 0)
    }

    fun addControlChange(voiceNumber: Int, controller: Int, value: Int, tickPosition: Long) {
        val channel = (voiceNumber - 1).coerceIn(0, 3)
        val ctrl = controller.coerceIn(0, 127)
        val valByte = value.coerceIn(0, 127)
        sequence.addControlChange(channel, ctrl, valByte, tickPosition)
    }

    fun process(
        pitches: List<MidiPitch>,
        markers: List<MidiMarkers> = emptyList(),
        initialBpm: Float = 120.0f,
        voiceInstruments: Map<Int, Int> = mapOf(1 to 0, 2 to 0, 3 to 0, 4 to 0),
        initialKey: String = "C"
    ) {
        val lastTick = 0
        nextTick.clear()
        // addMetaMessage(0x59, 4, 2, 2,0)
        tick = 0
        setTempoMeta(initialBpm, 0)
        voiceInstruments.forEach { (voiceIndex, programNumber) ->
            setProgramChange(voiceNumber = voiceIndex, instrumentProgram = programNumber, tickPosition = 0L)
        }

        val nuanceMap = mutableMapOf<Long, Int>()
        val fermataTicks = mutableSetOf<Long>()
        val textMetaMap = mutableMapOf<Long, MutableList<String>>()
        val keyChangeMap = mutableMapOf<Long, String>()
        val tempoChangeMap = mutableMapOf<Long, Float>()
        val hairpinMap = mutableMapOf<Long, Pair<String, Long>>()
        var pendingHairpinType: String? = null
        var pendingHairpinStartTick: Long? = null

        val modulaRegex = Regex("""D(ô|o)?\s*dia\s*(C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B)""", RegexOption.IGNORE_CASE)
        val tempoRegex = Regex("""^♩\s*=\s*(\d+)""", RegexOption.IGNORE_CASE)

        markers.forEach { midiMarker ->
            val gridIdx = (midiMarker.gridIndex ?: 0) * 480L
            midiMarker.marker.forEach { text ->
                val cleanText = text.trim()
                val vel = getVelocityFromNuance(cleanText, -1)
                val tempoMatch = tempoRegex.find(cleanText)
                val modMatch = modulaRegex.find(cleanText)
                /* Les nuances */
                if (vel != -1) {
                    nuanceMap[gridIdx] = vel
                }

                /* tempo */
                if (tempoMatch != null) {
                    val bpmValue = tempoMatch.groupValues[1].toFloatOrNull()
                    if (bpmValue != null) tempoChangeMap[gridIdx] = bpmValue
                }
                if (text.contains("𝄐")) {
                    fermataTicks.add(gridIdx)
                }

                /* Modulation */
                if (modMatch != null) {
                    val targetKey = modMatch.groupValues[2]
                    keyChangeMap[gridIdx] = targetKey
                }

                /* Hairpins */
                val isCres = cleanText == "<" || cleanText.lowercase().startsWith("cres")
                val isDecres = cleanText == ">" || cleanText.lowercase().startsWith("decresc")

                if (isCres || isDecres) {
                    if (pendingHairpinStartTick != null && pendingHairpinType != null) {
                        val defaultEnd = pendingHairpinStartTick!! + (2 * 480L)
                        hairpinMap[pendingHairpinStartTick!!] = Pair(pendingHairpinType!!, defaultEnd)
                    }

                    pendingHairpinType = if (isCres) "<" else ">"
                    pendingHairpinStartTick = gridIdx
                } else if (cleanText == "=" && pendingHairpinStartTick != null && pendingHairpinType != null) {
                    hairpinMap[pendingHairpinStartTick!!] = Pair(pendingHairpinType!!, gridIdx)
                    pendingHairpinType = null
                    pendingHairpinStartTick = null
                }

                if (!isCres && !isDecres && cleanText != "<" && cleanText != ">" && cleanText != "=" && !cleanText.contains("𝄐") && vel == -1 && tempoMatch == null && modMatch == null) {
                    textMetaMap.getOrPut(gridIdx) { mutableListOf() }.add(text)
                }
            }
        }

        val processedFermataTicks = mutableSetOf<Long>()
        val pendingSustainOff = mutableListOf<Pair<Long, Int>>()



        var timeOffset = 0L
        val holdDuration = 480L *3 // 480L:2noire *2:3noir *4:ronde

        if (pendingHairpinStartTick != null && pendingHairpinType != null) {
            val defaultEnd = pendingHairpinStartTick!! + (2 * 480L)
            hairpinMap[pendingHairpinStartTick!!] = Pair(pendingHairpinType!!, defaultEnd)
        }
        var currentKey = initialKey
        var baseVelocity = Dynamic.MF.velocity
        var baseExpression = 64                // Expression MIDI standard (0 à 127)
        val stepExpressionDelta = 10
        val baseKeyIdx = Transpose.keyToNumber.indexOf(initialKey).coerceAtLeast(0)

        val stepDelta = 20
        val processedHairpins = mutableSetOf<Long>()

        val pitchesByTick = pitches.groupBy { (it.tick) * 8L }.toSortedMap()
        pitchesByTick.forEach { (rawTick, eventsAtTick) ->

            /* Fermata CC*/
            val adjustedTickForRelease = rawTick + timeOffset
            val iterator = pendingSustainOff.iterator()
            while (iterator.hasNext()) {
                val (offTick, voiceNum) = iterator.next()
                if (offTick <= adjustedTickForRelease) {
                    addControlChange(voiceNumber = voiceNum, controller = 64, value = 0, tickPosition = offTick)
                    iterator.remove()
                }
            }

            var isFermataAtThisTick = false
            if (fermataTicks.contains(rawTick) && !processedFermataTicks.contains(rawTick)) {
                processedFermataTicks.add(rawTick)
                isFermataAtThisTick = true

                val fermataStartTick = rawTick + timeOffset
                val offTickTarget = fermataStartTick + holdDuration

                (1..4).forEach { v ->
                    addControlChange(voiceNumber = v, controller = 64, value = 127, tickPosition = fermataStartTick)
                    pendingSustainOff.add(Pair(offTickTarget, v))
                }
            }


            /* Nuances */
            if (nuanceMap.containsKey(rawTick)) {
                baseVelocity = nuanceMap[rawTick] ?: Dynamic.MF.velocity
            }

            val adjustedTick = rawTick/* + timeOffset*/



            // hairpin
            hairpinMap.forEach { (startTick, pair) ->
                val (type, endTick) = pair
                if (rawTick >= endTick && !processedHairpins.contains(startTick)) {
                    if (type == "<") {
                        baseExpression = (baseExpression + stepDelta).coerceIn(0, 127)
                    } else if (type == ">") {
                        baseExpression = (baseExpression - stepDelta).coerceIn(0, 127)
                    }
                    processedHairpins.add(startTick)
                }
            }
            var currentExpression = baseExpression

            val activeHairpin = hairpinMap.entries.firstOrNull { (start, pair) ->
                rawTick >= start && rawTick < pair.second
            }

            if (activeHairpin != null) {
                val startTick = activeHairpin.key
                val (type, endTick) = activeHairpin.value

                if (endTick > startTick) {
                    val progress = (rawTick - startTick).toDouble() / (endTick - startTick).toDouble()

                    if (type == "<") {
                        currentExpression = (baseExpression + (stepExpressionDelta * progress)).toInt().coerceIn(0, 127)
                    } else if (type == ">") {
                        currentExpression = (baseExpression - (stepExpressionDelta * progress)).toInt().coerceIn(0, 127)
                    }

                    (1..4).forEach { voiceNum ->
                        addControlChange(voiceNumber = voiceNum, controller = 11, value = currentExpression, tickPosition = rawTick)
                    }
                }
            }


            // Tempo Meta 0x51
            val pendingTempoTicks = tempoChangeMap.keys.filter { it <= rawTick }.sorted()
            pendingTempoTicks.forEach { tempoRawTick ->
                val newBpm = tempoChangeMap[tempoRawTick] ?: initialBpm
                val tempoAdjustedTick = tempoRawTick/* + timeOffset*/

                setTempoMeta(newBpm, tempoAdjustedTick.toInt())
                tempoChangeMap.remove(tempoRawTick)
            }

            // Modulation Meta 0x59
            val pendingKeyTicks = keyChangeMap.keys.filter { it <= rawTick }.sorted()
            pendingKeyTicks.forEach { keyRawTick ->
                val targetKey = keyChangeMap[keyRawTick] ?: initialKey
                currentKey = targetKey

                val keyAdjustedTick = keyRawTick/* + timeOffset*/
                val keyIdx = Transpose.keyToNumber.indexOf(targetKey)

                if (keyIdx != -1) {
                    val sf = Transpose.keyToArmature[keyIdx]
                    val keyBytes = byteArrayOf(sf.toByte(), 0.toByte()) // 0 = Majeur

                    addMetaMessage(0x59, keyAdjustedTick.toInt(), 2, keyBytes)
                }
                keyChangeMap.remove(keyRawTick)
            }


            /* Tetxe; marker */
            val pendingTextTicks = textMetaMap.keys.filter { it <= rawTick }.sorted()
            pendingTextTicks.forEach { textRawTick ->
                val textAdjustedTick = textRawTick/* + timeOffset*/

                textMetaMap[textRawTick]?.forEach { annotationText ->
                    addTextMeta(text = annotationText, tickPosition = textAdjustedTick, metaType = 0x06)
                }
                textMetaMap.remove(textRawTick)
            }

            eventsAtTick.forEach { noteEvent ->
                // Note + transpose
                val currentKeyIdx = Transpose.keyToNumber.indexOf(currentKey).coerceAtLeast(0)
                val transpositionSemitones = currentKeyIdx - baseKeyIdx

                if (noteEvent.metaType > 0) {
                    addMetaMessage(noteEvent.metaType, adjustedTick.toInt(), noteEvent.metaByteSize, noteEvent.metaBytes)
                } else if (noteEvent.pitch != "") {
                    val finalTransposedPitch = noteEvent.pitch.toInt() + transpositionSemitones
                    addNote(noteEvent.voiceNumber, finalTransposedPitch, baseVelocity, adjustedTick)
                }
            }

            if (fermataTicks.contains(rawTick)) {
                timeOffset += holdDuration
            }
        }

        tempoChangeMap.forEach { (tempoRawTick, bpm) ->
            val tempoAdjustedTick = tempoRawTick/* + timeOffset*/
            setTempoMeta(bpm, tempoAdjustedTick.toInt())
        }
        tempoChangeMap.clear()


        pendingSustainOff.forEach { (offTick, voiceNum) ->
            addControlChange(voiceNumber = voiceNum, controller = 64, value = 0, tickPosition = offTick)
        }
        pendingSustainOff.clear()
    }

    fun getVelocityFromNuance(markerText: String, defaultVelocity: Int = -1): Int {
        val dynamic = Dynamic.fromLabel(markerText)
        if (dynamic != null) {
            return dynamic.velocity
        }
        return -1
    }
    /**
     * (Type 0x01 = Text, 0x06 = Marker, 0x05 = Lyric).
     */
    fun addTextMeta(text: String, tickPosition: Long, metaType: Int = 0x01) {
        val bytes = text.toByteArray(Charsets.UTF_8)
        addMetaMessage(
            type = metaType,
            tick = tickPosition.toInt(),
            nbData = bytes.size,
            metaBytes = bytes
        )
    }
}