package mg.dot.feufaro.solfa

import SharedScreenModel
import androidx.compose.runtime.remember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.SaveSettings
import mg.dot.feufaro.getConfigDirectoryPath
import mg.dot.feufaro.launchFilePicker
import mg.dot.feufaro.midi.MidiPitch
import mg.dot.feufaro.midi.MidiWriterKotlin

//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
//@todo: ffpm-172-2 ${O:1} non fonctionnel
//@todo: ffpm-164.txt O10:s3:mflswss // alternative notes for stanza 3 only
//@todo: ews-47.txt DS/$ + [12][3]
//@todo: ffpm-99-2.txt triolet.mid + triolet (m--)
//@todo: ffpm-657-1.txt ${m:4/4 Miadana kely}
//@todo: ews-80.txt ${m:12/4}
//@todo: ews-81.txt ${R=}${D:....}
//@todo: bug ffpm-629.txt sssszd au lieu de ssss-d(-rrrrtdr)
//@todo: bug ews-103.txt Triolet adds one blank
//@todo: bug ffpm-167-1.txt shoud start with : and not |

class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository: FileRepository) {
    private val T: MutableList<PTemplate> = mutableListOf()
    private val N: MutableList<POneVoiceNote> = mutableListOf()
    private val L: MutableList<POneStanzaLyrics> = mutableListOf()
    private val O: MutableMap<Int, MutableList<String>> = mutableMapOf()
    private val unparsedNote = mutableListOf<String>()
    private var refrainBeginsAt = -1
    private var smartLyricsType = "L"
    private val pitches = mutableListOf<MidiPitch>()

    companion object {
        var currentFile = ""
        val REGEX_SHIFT_PAREN_UPWARD = Regex("([^a-yA-Y\\(\\[]+)([\\)\\]])")
        val REGEX_SHIFT_PAREN_DOWNWARD = Regex("([\\(\\[])([^a-yA-y\\)\\]]+)")
        val REGEX_COLLAPSE_PARENS_OPEN = Regex("([\\(\\[])[\\(\\[]+")
        val REGEX_COLLAPSE_PARENS_CLOSE = Regex("([\\)\\]])[\\)\\]]+")
        val REGEX_SINGLETON_PAREN = Regex("[\\(\\[](.[¹²³₁₂₃]?)?[\\)\\]]") // @todo
        val REGEX_SHIFT_COMMA_UPWARD = Regex("([^a-zA-Z]+)([',])")
        val REGEX_TEMPLATE_COMMENT = Regex("(\\$[A-Z]|\\$\\{[^\\}]*\\})")
        val REGEX_REPETITION = Regex("([zwdrmfslt-](?>[',]*))([1-9][0-9]*)", RegexOption.IGNORE_CASE)
        val REGEX_PARSE_META = Regex("\\|(?=[a-z]:)")
        val REGEX_LYRICS_COMMENT = Regex("\\$\\{([^\\}][^:]*:([^\\}]*))\\}|\\$\\{R!\\}")
        val REGEX_LYRICS_REPETITION = Regex("_(\\d+)")
        val REGEX_VOWELS_STAGE1 =
            Regex("[aeiouyòàéìỳ](?![,;\\.\\-:!\\?\\}»_\"]*([ aeiouyòàéìỳ/]|_[1-9]))", RegexOption.IGNORE_CASE)
        val REGEX_VOWELS_STAGE2 = Regex("(?<=[aeiouyòàéìỳ])_([,;\\.\\-:!\\?\\}»_\"]+)", RegexOption.IGNORE_CASE)
        val REGEX_VOWELS_STAGE3 = Regex("_([\\?\\!:,;\\.»\\)]+|(\\$\\{[^\\}]*\\})+)")
        val REGEX_MALAGASY_MN = Regex("([aeio])_([nm])([tdjkbp])")
        val REGEX_MALAGASY_MN_STAGE2 = Regex("_([mn])-")
        val REGEX_PAREN_RECURSIVE = Regex("(\\([^\\(\\)]*)\\(([^\\)]*)\\)")
        val REGEX_COMMENT = Regex("\\$\\{[^\\}]*\\}")
        val REGEX_STRIP_DC = Regex("\\$\\{D[^:]*:[^\\}]*\\}")
    }

    var nextTIndex: Int = -1
    var nextNIndex: Int = -1
    var nextLIndex: Int = -1
    var inGroup: Boolean = false
    var templateString: String = ""

    private val meta: MutableMap<String, String> = mutableMapOf()
    private val lyricsComment: MutableList<String> = mutableListOf()
    suspend fun nextTimeUnitObject() {
        val lastTUO = sharedScreenModel.lastTUO()
        nextTIndex++
        if (nextTIndex == 5) {
            sharedScreenModel.doneTUOList()
        }
        if (T.getOrNull(nextTIndex) == null) {
            sharedScreenModel.doneTUOList()
            return
        }
        val pTemplate = T[nextTIndex]
        val unitObject = TimeUnitObject(pTemplate, lastTUO, N.size)
        if (lastTUO == null) {
            unitObject.setOldKey(sharedScreenModel.songKey.value)
        } else {
            unitObject.setOldKey(lastTUO.transposeOldKey)
        }
        val templateCharArray = pTemplate.template.toCharArray()
        var nextMarker: Char = '0'
        templateCharArray.forEach {  // D.-R
            when (it) {
                'D', 'R', 'M', 'F', 'S', 'L', 'T',
                'd', 'r', 'm', 'f', 's', 'l', 't',
                    -> {
                    nextNIndex++
                    if (!inGroup) {
                        nextLIndex++
                        if (L.size > nextLIndex) {
                            unitObject.addLyrics(L[nextLIndex])
                        }
                    }
                    N.forEachIndexed { voiceNumber, pOneVoiceNote ->
                        run {
                            unitObject.addNote(voiceNumber, pOneVoiceNote.oneVoiceNote, nextNIndex, nextMarker)
                        }
                    }
                    nextMarker = '0'
                }

                '.', ',' ->
                    unitObject.addMarker(it)

                '-', 'Z', 'z', 'w' ->
                    N.indices.forEach { voiceNumber ->
                        unitObject.addBlank(voiceNumber, it)
                    }

                '(', '[' -> {
                    nextLIndex++
                    if (L.size > nextLIndex) {
                        unitObject.addLyrics(L[nextLIndex])
                    }
                    inGroup = true
                }

                ')', ']' -> {
                    inGroup = false
                }
            }
        }
        withContext(Dispatchers.Main) {
            sharedScreenModel.addTUO(unitObject)
        }
        nextTimeUnitObject()
    }

    fun loadSolfa() {
        val sourceFileName: String = sharedScreenModel.currentPlayed()
        sharedScreenModel.reset()
        parse(sourceFileName)
    }

    fun loadNextInPlaylist() {
        val playlist = sharedScreenModel.playlist.value
        if (playlist.isNotEmpty()) {
            sharedScreenModel.playNext()
            loadSolfa()
        }
    }

    fun loadFile() {
        val screenModelScope= CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        val stateSettings = SaveSettings()

        screenModelScope.launch {
            val initialPath = stateSettings.loadLastUsedDir()
            val homedir = getConfigDirectoryPath();

            launchFilePicker(
                mimeTypes = arrayOf("text/plain"),
                initialDirectory = if (initialPath.isNotBlank()) initialPath else homedir,
                onFileSelected = { path ->
                    if (path != null) {
                        screenModelScope.launch(Dispatchers.Default) {
                            sharedScreenModel.reset()
                            parse(path)
                            stateSettings.saveLastUsedDir(path)
                        }
                        //loadSolfa()
                    } else {
                        println("Pas de dichier")
                    }
                }
            )
        }
    }

    fun resetShared() {
        sharedScreenModel.reset()
    }
    fun parse(sourceFile: String) {
        println("37: lFL bien reçu $sourceFile")
        currentFile = sourceFile
        val parseScope = CoroutineScope(Dispatchers.Default)
        parseScope.launch {
            val lines = try {
                fileRepository.readFileLines(sourceFile)
            } catch (e: Exception) {
                println("Opening $sourceFile raised exception {${e.message}")
                emptyList()
            }
            val contentString = fileRepository.readFileLines(sourceFile).joinToString("\n")
            sharedScreenModel.setFileContent(contentString, sourceFile)
            O.clear()
            T.clear()
            N.clear()
            L.clear()
            if (pitches.isNotEmpty()) {
                pitches[0].reset()
            }
            pitches.clear()
            refrainBeginsAt = -1
            unparsedNote.clear()
            templateString = ""
            nextTIndex = -1
            nextNIndex = -1
            nextLIndex = -1
            lyricsComment.clear()
            if (sharedScreenModel.stanza.value == 0) {
                sharedScreenModel.setStanza(1)
            }
            sharedScreenModel.setNbStanzas(0)
            TimeUnitObject.hasMarker(false)
            lines.forEach { line ->
                run {
                    parseOneLine(line)
                }
            }
            unparsedNote.forEach {
                preloadN(it)
            }
            try {
                nextTimeUnitObject()
            } catch (e: Exception) {
                println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
            }
            val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
            val midiWriter = MidiWriterKotlin(fileRepository)
            midiWriter.process(pitches)
            midiWriter.save("whawyd3.mid")
            sharedScreenModel.setStanza(1)
        }
    }

    private fun rearrangeNote(noteString: String, infiniteIter: Int = 0): String {
        var result: String = noteString
        result = result.replace(REGEX_PAREN_RECURSIVE, "$1$2")
        result = result.replace(REGEX_PAREN_RECURSIVE, "$1$2")
        result = result.replace(REGEX_PAREN_RECURSIVE, "$1$2")
        result = result.replace(REGEX_SHIFT_PAREN_UPWARD, "$2$1")
        result = result.replace(REGEX_SHIFT_PAREN_DOWNWARD, "$2$1")
        result = result.replace(REGEX_COLLAPSE_PARENS_OPEN, "$1")
        result = result.replace(REGEX_COLLAPSE_PARENS_CLOSE, "$1")
        result = result.replace(REGEX_SINGLETON_PAREN, "$1")
        result = result.replace(REGEX_SHIFT_COMMA_UPWARD, "$2$1")
        if (result != noteString && infiniteIter < 75) {
            result = rearrangeNote(result, infiniteIter + 1)
        }
        if (infiniteIter >= 70) {
            println("$infiniteIter\n$noteString\n$result")
        }
        return result
    }

    private fun preloadN(noteLine: String) {
        val voiceNumber = noteLine.substring(0, 1).toInt()
        val templateComments = mutableListOf<String>()
        val templateStripped = REGEX_TEMPLATE_COMMENT.replace(templateString) { matchResult ->
            templateComments.add(matchResult.value)
            "&"
        }
        val templateCharArray = templateStripped.toCharArray()
        val noteLine = noteLine.replace(REGEX_TEMPLATE_COMMENT, "")
            .replace(" ", "")
        val lineRepeated = REGEX_REPETITION.replace(noteLine.substring(1)) { matchResult ->
            val (charIterated, iteration) = matchResult.destructured
            val nTimes = iteration.toInt()
            charIterated.repeat(nTimes)
        }.replace(" ", "")
        var noteChar2 = ""
        var anchor = "d"
        var inAnchor = false
        var anchorNote = ' '
        lineRepeated.toCharArray().forEach {
            if (inAnchor) {
                if (anchor.isEmpty()) {
                    anchorNote = it
                }
                if (anchor.isNotEmpty() && it !in listOf(',', '\'')) {
                    inAnchor = false
                } else {
                    anchor += it
                    return@forEach
                }
            }
            if (it == '#') {
                inAnchor = true
                anchor = ""
                return@forEach
            }
            noteChar2 += it
            val anchorSuffix = anchor.replace(Regex("^[drmfslt]"), "")
            if (it in listOf('d', 'r', 'm', 'f', 's', 'l', 't', 'D', 'R', 'M', 'F', 'S', 'L', 'T')) {
                noteChar2 += anchorSuffix
                if (
                    (anchorNote == 'r' && it in listOf('d', 'D'))
                    || (anchorNote == 'm' && it in listOf('d', 'D', 'r', 'R'))
                    || (anchorNote == 'f' && it in listOf('d', 'D', 'r', 'R', 'm', 'M'))
                    || (anchorNote == 's' && it !in listOf('s', 'S', 'l', 'L', 't', 'T'))
                    || (anchorNote == 'l' && it !in listOf('l', 'L', 't', 'T'))
                    || (anchorNote == 't' && it !in listOf('t', 'T'))
                ) {
                    noteChar2 += '\''
                }
            }
        }
        noteChar2 = noteChar2.replace(",'", "")
            .replace("',", "")
            .replace(",'", "")
            .replace("',", "")

        val noteCharIterator = noteChar2.replace("/", "").toCharArray().iterator()
        var result = ""
        var firstInLoop = true
        var replacement: Char = 'x'
        templateCharArray.forEach {
            when (it) {
                'D', 'R', 'M', 'F', 'S', 'L', 'T',
                'd', 'r', 'm', 'f', 's', 'l', 't',
                    -> {
                    if (firstInLoop) {
                        replacement = if (noteCharIterator.hasNext()) noteCharIterator.next() else 'd'
                        firstInLoop = false
                    }
                    if (replacement == '(') {
                        result += replacement
                        replacement = if (noteCharIterator.hasNext()) noteCharIterator.next() else 'd'
                    }
                    for (repetition in 0 until 6) {
                        result += replacement
                        replacement = if (noteCharIterator.hasNext()) noteCharIterator.next() else 'd'
                        if (replacement in setOf(
                                'd', 'r', 'm', 'f', 's', 'l', 't', 'z', '(',
                                'D', 'R', 'M', 'F', 'S', 'L', 'T', '-', 'w', 'Z'
                            )
                        ) {
                            break
                        }
                    }
                }

                'w', '-' -> result += "~"
                'z', 'Z' -> result += "@"
                ',' -> result += ";"
                else -> result += it
            }
        }
        result = result.replace("w", "-")
        result = rearrangeNote(result)
        loadN(voiceNumber, result, templateComments)
    }

    private suspend fun parseOneLine(line: String, regexIgnoreString: String = "") {
        val index: Int
        val value: String
        val lineLength = line.trim().length
        if (lineLength <= 3) {
            return
        }
        val key: String = line.trim().substring(0, 1)
        if (lineLength > 4 && line.trim().substring(3, 4) == ":") {
            index = line.substring(1, 3).toIntOrNull() ?: 0
            value = line.trim().substring(4)
        } else {
            index = line.substring(1, 2).toIntOrNull() ?: 0
            value = line.trim().substring(3)
        }

        when (key) {
            "T" -> if (index == 0) {
                loadT(value)
            }

            "N" ->
                // Les lignes de type N seront parsées à la fin. preloadN() se chargera d'abord de réarranger la ligne
                // pour bien gérer les parenthèses.
                unparsedNote.add(index.toString() + value)

            "M" ->
                loadM(value)

            "L" ->
                loadL(index, value)

            "Y" ->
                loadY(index, value)

            "E" ->
                loadE(index, value)

            "U" ->
                loadU(value)

            "O" ->
                loadO(index, value)

            "I" ->
                loadI(value, regexIgnoreString)
        }

    }

    suspend fun loadI(line: String, regexIgnoreString: String = "") {
        val lineSplit = line.split(":")
        val lastSlashInFilename = currentFile.lastIndexOf('/')
        val fileNamePrefix = if (lastSlashInFilename != -1) {
            currentFile.substring(0, lastSlashInFilename + 1)
        } else {
            ""
        }
        val regexIgnoreString = try {
            if (regexIgnoreString != "") {
                regexIgnoreString + "|" + lineSplit[1]
            } else {
                lineSplit[1]
            }
        } catch (e: Exception) {
            "^:"
        }
        val filePath = fileNamePrefix + lineSplit[0]
        val lines = try {
            fileRepository.readFileLines(filePath)
        } catch (e: Exception) {
            println("Opening $filePath raised exception {${e.message}")
            emptyList()
        }
        try {
            val ignoredLines = Regex(regexIgnoreString)
            lines.forEach {
                val match = ignoredLines.find(it)
                if (match == null) {
                    parseOneLine(it, regexIgnoreString)
                }
            }
        } catch (e: Exception) {

        }
    }

    private fun loadM(value: String) {
        val metaChunks: List<String> = value.split(REGEX_PARSE_META)
        metaChunks.forEach { parseMeta(it) }
        parseMeta(value)
        sharedScreenModel.setMeasure(meta["m"] ?: "")
        sharedScreenModel.setSongTitle(meta["t"] ?: "")
        sharedScreenModel.setSongAuthor(meta["a"] ?: "")
        sharedScreenModel.setSongComposer(meta["h"] ?: "")
        sharedScreenModel.setSongRhythm(meta["r"] ?: "")
    }

    private fun loadN(voiceNumber: Int, line: String, templateComments: MutableList<String>) {
        var midiDuration = 0
        var nextDuration = 0
        var lastNoteString = ""
        val midiPitch = MidiPitch(voiceNumber)
        var nextComment: MutableList<String> = mutableListOf()
        fun pushMidi(typeBlock: String = "note") {
            if (typeBlock == "measure") {
                val z = midiPitch.copy()
                if (z.tick != 0) {
                    val tickMeta = midiPitch.tick
                    val numerator = midiPitch.metaBytes.toByteArray()[0].toInt()
                    val denominatorPower = midiPitch.metaBytes.toByteArray()[1].toInt()
                    val denominator = 1 shl denominatorPower
                    z.tick = 0
                    val newNumerator = tickMeta / 15 / denominator
                    z.metaBytes = "" + newNumerator.toChar() + denominatorPower.toChar() + 60.toChar() + 20.toChar()

                    pitches.add(z)
                }
                return;
            }
            if (midiDuration > 0 && typeBlock == "note") {
                midiPitch.setNote(lastNoteString, midiDuration)
                pitches.add(midiPitch.copy())
                midiPitch.setMarker(nextComment)
            } else if (typeBlock == "meta") {
                pitches.add(midiPitch.copy())
            }
            midiPitch.metaBytes = ""
            midiPitch.metaType = -1
            midiPitch.duration = 0
            lastNoteString = ""
            midiDuration = 0
            nextComment = mutableListOf()
        }

        midiPitch.currentVoiceNumber(voiceNumber)
        if (voiceNumber == 1) {
            midiPitch.initKey(meta["C"] ?: "C")
            pushMidi("meta")
            if (midiPitch.initMeasure(meta["m"] ?: "4/4")) {
                pushMidi("measure")
                pushMidi("meta")
            }
        }
        var lastIt = ' '
        val newN = POneVoiceNote()
        val lineRepeated = REGEX_REPETITION.replace(line) { matchResult ->
            val (charIterated, iteration) = matchResult.destructured
            val nTimes = iteration.toInt()
            charIterated.repeat(nTimes)
        }
        var commentNumber = -1
        val lineChar = lineRepeated.toCharArray()
        lineChar.forEach {
            when (it) {
                '|', ':', '!', '/' -> {
                    if (lastIt == '|' || lastIt == ':' || lastIt == '!' || lastIt == '/') {
                    } else {
                        midiDuration += nextDuration
                        nextDuration = 60
                    }
                }

                '.' -> {
                    if (lastIt == ';') {
                        pushMidi()
                        lastNoteString = "z"
                        midiDuration = 15
                        nextDuration = 30
                    }
                    midiDuration += nextDuration - 30
                    nextDuration = 30
                }

                ';' -> {
                    if (lastIt == '.') {
                        pushMidi()
                        lastNoteString = "z"
                        midiDuration = 15
                        nextDuration = 15
                    }
                    if (nextDuration >= 45) {
                        midiDuration += nextDuration - 45
                        nextDuration = 45
                    } else {
                        midiDuration += nextDuration - 15
                        nextDuration = 15
                    }
                }

                '\'', ',' -> {
                    newN.appendLastNote(it)
                    lastNoteString += it
                }

                '(', '[' -> newN.addNextMarker(it)
                ')', ']' -> newN.appendLastMarker(it)
                'd', 'r', 'm', 'f', 's', 'l', 't',
                'D', 'R', 'F', 'S', 'T',
                '-', 'Z', 'z'
                    -> {
                    var noteString = it.toString().lowercase()
                    if (it == 'D' || it == 'R' || it == 'F' || it == 'S') {
                        noteString += "i"
                    }
                    if (it == 'T') {
                        noteString += "a"
                    }
                    newN.addItem(noteString)
                    when (noteString) {
                        "-" -> {
                            midiDuration += nextDuration
                            nextDuration = 0
                        }

                        "z", "@" -> {
                            if (lastNoteString == "z") {
                                midiDuration += nextDuration
                                nextDuration = 0
                            } else {
                                pushMidi()
                                //noteString =
                            }
                            lastNoteString = "z"
                        }

                        "'", "(", ")", "[", "]", "," -> {}
                        else -> {
                            pushMidi()
                            lastNoteString = noteString
                        }
                    }
                }

                '@' -> {
                    if (lastNoteString == "z") {
                        midiDuration += nextDuration
                        nextDuration = 0
                    } else {
                        pushMidi()
                    }
                    lastNoteString = "z"
                }

                'i', 'a' ->
                    lastNoteString += it

                '&' -> {
                    commentNumber++
                    nextComment.add(templateComments[commentNumber])
                }
            }
            lastIt = it
        }
        pushMidi()
        /*if (voiceNumber == 1) {
            N.add(0, POneVoiceNote())
        }*/
        N.addWithPadding(voiceNumber, newN)
        midiPitch.setNote("z", 0)
        pitches.add(midiPitch.copy())
    }

    private fun loadT(line: String) {
        templateString = line
        var nextTemplate = ""
        val nextMarker: MutableList<PMarkers> = mutableListOf()
        var tMarker = ""
        val lineChar = line.toCharArray()
        var offset: Int = 0
        var templateOffset = 0
        lineChar.forEach {
            if (tMarker != "") {
                tMarker += it
                if ((it == '}')
                    || (tMarker.length == 2 && it != '{')
                ) {
                    TimeUnitObject.hasMarker(true)
                    nextMarker.add(PMarkers(tMarker, offset - templateOffset))
                    tMarker = ""
                }
                return@forEach
            }
            if (it == '$') {
                tMarker = "$"
                return@forEach
            }
            offset++
            if (it == ':' || it == '|' || it == '/' || it == '!') {
                val newTemplateItem =
                    PTemplate(nextTemplate, it.toString(), nextMarker.toMutableList())
                T.add(newTemplateItem)
                nextMarker.clear()
                nextTemplate = ""
                templateOffset = offset
            } else {
                nextTemplate += it
            }
        }
        val newTemplateItem = PTemplate(nextTemplate, "}", nextMarker)
        T.add(newTemplateItem)
    }

    fun loadU(line: String) {
        val measureLong = meta.getOrElse("m") { "4/4" }
        val measure = measureLong.split("/")[0].toIntOrNull() ?: 4
        val uObject = ParseULine(line, measure)
        val parsedULine = uObject.parsed()
        loadT(parsedULine)
    }

    fun loadO(index: Int, line: String) {
        O.getOrPut(index) { mutableListOf() }.add(line)
    }

    fun parseMeta(line: String) {
        /* $_a_keyAbbrev = array(
  'a' => 'author',
  'c' => 'tonality',
  'h' => 'composer',
  'i' => 'interline',
  'l' => 'lyrics font size',
  'm' => 'measure',
  'n' => 'note font size',
  'r' => 'rhythm',
  't' => 'title',
);*/
        if (line.trim() == "") {
            return
        }
        val subKey: String = line.substring(0, 1).trim()
        if (subKey == "|") {
            return
        }
        if (subKey != "" && "M" != subKey) {
            meta[subKey] = line.substring(2)
        }
        if ("c" == subKey) {
            val tonality = line.uppercaseFirstMeta()
            meta["C"] = tonality
            sharedScreenModel.setSongKey(tonality)
            sharedScreenModel.setTransposeAsIf(tonality)
        }
    }

    private fun String.uppercaseFirstMeta(): String {
        if (this.length < 2) {
            return ""
        }
        return get(2).uppercaseChar() + substring(3)
    }

    private fun String.addHyphens(): String {
        return this
            .replace(Regex("(?<![\\?:,\\.; ])_"), "-_")
            .replace(Regex("_-(?=_)"), "_")
            .replace(" -_", "_")
            .replace("--_", "-_")
            .replace(Regex("_$"), "")
    }

    private fun unpackLyrics(lyrics: String): String {
        val comments = REGEX_COMMENT.findAll(lyrics)
        val commentsIterator = comments.iterator()
        val loadedLyrics = lyrics//.replace(REGEX_LYRICS_COMMENT, "")
            .replace(REGEX_LYRICS_REPETITION) { matchResult ->
                val repeating = matchResult.destructured.match.groupValues[1]
                "_".repeat(repeating.toString().toInt())
            }
            .addHyphens()
        val lyricsFinal = REGEX_COMMENT.replace(loadedLyrics) { matchResult ->
            commentsIterator.next().value
        }
        return lyricsFinal
    }

    private fun loadL(stanzaNumber: Int, lyrics: String) {
        val unpackedLyrics = unpackLyrics(lyrics)
        loadLyrics(stanzaNumber, unpackedLyrics)
    }

    fun setOverrideLyrics(stanzaNumber: Int, i: Int, voice: Int, text: String) {
        while (L.size <= i) {
            L.add(POneStanzaLyrics())
        }
        L[i].setAlternativeLyrics(stanzaNumber, voice, text)
    }

    private fun loadLyrics(stanzaNumber: Int, lyrics: String) {
        val overrideIterator: MutableMap<Int, Iterator<String>> = mutableMapOf()
        try {
            val lyrics = getLyricsComments(lyrics)
            val arrayLyrics = lyrics.split(Regex("[/_]"))
            arrayLyrics.forEachIndexed { i, lyricsItem ->
                val originalLyrics = REGEX_STRIP_DC.replace(lyricsItem, "")
                val REGEX_OVERRIDE = Regex(".*\\$\\{O:(\\d+)\\}.*")
                if (REGEX_OVERRIDE.matches(lyricsItem)) {
                    val overrideNumber = lyricsItem.replace(REGEX_OVERRIDE, "$1").toInt()
                    val override = O.getOrElse(overrideNumber) { null }
                    override?.forEachIndexed { index, value ->
                        val matchResult = Regex("v(\\d+):(.*)").find(value)
                        // 1. Extraire le numéro de voix (ex: "2")
                        val voiceNum = matchResult?.groupValues?.get(1) ?: ""
                        val prefix = if (voiceNum.isNotEmpty()) {
                            // .map transforme chaque caractère '3', '4' en "3.", "4."
                            // .joinToString("") les colle ensemble -> "3.4."
                            voiceNum.map { "$it." }.joinToString("")
                        } else {
                            ""
                        }
                        val overrideString = when (smartLyricsType) {
                            "E" -> smartELyrics(matchResult?.groupValues[2] ?: value)
                            "Y" -> smartYLyrics(matchResult?.groupValues[2] ?: value)
                            else -> value
                        }
                        for (iN in (1..9)) {
                            if (matchResult?.groupValues[1]?.contains(iN.digitToChar()) ?: false) {
                                val overrideSyllabus = overrideString.split(Regex("[_/]"))
                                    .map { syllable ->
                                        if (syllable.isNotBlank()) "$prefix$syllable" else syllable
                                    }
                                    .iterator()
                                overrideIterator[iN] = overrideSyllabus
                            }
                        }
                    }
                }
                if (overrideIterator.isNotEmpty()) {
                    overrideIterator.map { (voice, textIterator) ->
                        if (textIterator.hasNext()) {
                            setOverrideLyrics(stanzaNumber, i, voice, textIterator.next())
                        }
                    }
                }
                addLyricsItem(stanzaNumber, i, originalLyrics)
            }
            if (refrainBeginsAt > 0 && stanzaNumber > 1 && !lyrics.contains($$"${R!}")) {
                copyRefrainToStanza(stanzaNumber)
            }

            if (lyricsComment.isNotEmpty()) {
                val lyricsIterator = lyricsComment.iterator()
                while (lyricsIterator.hasNext()) {
                    var item = lyricsIterator.next()
                    var positionDS = 0
                    // @todo: if item == ${O:...}
                    // ${D:xxx} lyrics of DC and DSs
                    val isDS = Regex("^\\$\\{D([^:]*):").find(item)
                    if (isDS != null) {
                        item = item.replace(REGEX_LYRICS_COMMENT, "$2")
                        val signDS = isDS.groupValues[1]
                        //todo: calculate positionDS
                        val item = when (smartLyricsType) {
                            "E" -> smartELyrics(item.replace(" _", " "))
                            "Y" -> smartYLyrics(item)
                            else -> item
                        }
                        item
                            .addHyphens()
                            .split(Regex("[_/]"))
                            .forEachIndexed { i, xval ->
                                appendLyricsItem(stanzaNumber, i + positionDS, xval)
                            }
                        lyricsIterator.remove()
                    }
                }
            }
        } catch (e: Exception) {
            sharedScreenModel.appendData("\nErr: $e")
        }
        if (stanzaNumber > sharedScreenModel.nbStanzas.value) {
            sharedScreenModel.setNbStanzas(stanzaNumber)
        }
        lyricsComment.clear()
    }

    fun smartYLyrics(lyrics: String): String {
        // Les ${O:1} risquent de changer en ${O:_1}. Sauvegardons-les dans comments.
        val comments = REGEX_COMMENT.findAll(lyrics)
        val commentsIterator = comments.iterator()
        val loadedLyrics = lyrics
            .replace(REGEX_VOWELS_STAGE1, "$0_")
            .replace(REGEX_VOWELS_STAGE2, "$1_")
            .replace(REGEX_VOWELS_STAGE3, "$1_")
            .replace(" ", " _")
            .replace("_\\ _", " ")
            .replace("_\\", "")
            .replace("_0", "")
            .replace(REGEX_MALAGASY_MN, "$1$2_$3")
            .replace(REGEX_MALAGASY_MN_STAGE2, "$1-_")
            .replace("_n'", "n'_")
        // Et remettons les valeurs originales de ${...}
        val lyricsFinal = REGEX_COMMENT.replace(loadedLyrics) { matchResult ->
            commentsIterator.next().value
        }
        return unpackLyrics(lyricsFinal)
    }

    // loadY is a smart lyrics parser for Malagasy language
    private fun loadY(intKey: Int, lyrics: String) {
        smartLyricsType = "Y"
        val smartLyrics = smartYLyrics(lyrics)
        loadL(intKey, smartLyrics)
    }

    fun smartELyrics(lyrics: String): String {
        val loadedLyrics = lyrics
            .replace(" ", " _")
            .replace("\\ _", " ")
            .replace("_\\", "")
            .replace("_/", "/")
            .replace("_0", "")
        //getLyricsComments(loadedLyrics)
        return unpackLyrics(loadedLyrics)
    }

    // loadE is a smart Lyrics parser for English language
    private fun loadE(intKey: Int, lyrics: String) {
        smartLyricsType = "E"
        val smartLyrics = smartELyrics(lyrics)
        loadL(intKey, smartLyrics)
    }

    private fun getLyricsComments(lyrics: String): String {
        val matchResult = REGEX_LYRICS_COMMENT.findAll(lyrics)
            .map { it.value }.toList()
        if (matchResult.isNotEmpty()) {
            lyricsComment.addAll(matchResult)
        }
        return REGEX_STRIP_DC.replace(lyrics, "")
    }

    private fun defineRefrainFrom(tuoNumber: Int) {
        refrainBeginsAt = tuoNumber
    }

    private fun copyRefrainToStanza(stanzaNumber: Int) {
        for (i in refrainBeginsAt..L.size - 1) {
            L[i].setLyrics(stanzaNumber, L[i].getLyrics(1))
            L[i].copyAlternativeLyrics(1, stanzaNumber)
        }
    }

    private fun addLyricsItem(stanzaNumber: Int, i: Int, lyricsItem: String) {
        while (L.size <= i) {
            L.add(POneStanzaLyrics())
        }
        val strippedLyrics =
            if (lyricsItem.contains($$"${R=}")) {
                if (stanzaNumber == 1) {
                    defineRefrainFrom(i)
                }
                lyricsItem.replace($$"${R=}", "")
            } else {
                lyricsItem
            }
        L[i].setLyrics(stanzaNumber, strippedLyrics)
    }

    private fun appendLyricsItem(stanzaNumber: Int, i: Int, lyricsItem: String) {
        if (L.size > i) {
            L[i].appendDSLyrics(stanzaNumber, lyricsItem)
        }
    }

    fun MutableList<POneVoiceNote>.addWithPadding(index: Int, element: POneVoiceNote) {
        if (index < this.size) {
            // L'index existe déjà, on remplace l'élément.
            this[index] = element
        } else {
            // L'index n'existe pas encore.
            // On remplit d'abord les "trous" avec des notes par défaut.
            for (i in this.size until index) {
                this.add(POneVoiceNote())
            }
            // Puis on ajoute le nouvel élément à la fin de la liste,
            // ce qui est maintenant l'index cible.
            this.add(element)
        }
    }
}