package mg.dot.feufaro.midi

import SharedScreenModel
import com.russhwolf.settings.Settings
import kotlinx.coroutines.*
import org.billthefarmer.mididriver.MidiDriver
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import java.io.RandomAccessFile

actual class FMediaPlayer actual constructor(
    private val filename: String,
    private val sharedScreenModel: SharedScreenModel,
    private val onFinished: () -> Unit
): KoinComponent {
    private data class MidiEvent(
        val tickAbsolute: Long,
        val type: Int,
        val channel: Int,
        val data1: Int,
        val data2: Int,
        val metaType: Int = -1,
        val metaData: ByteArray = ByteArray(0)
    )

    private data class MidiSequence(
        val resolution: Int,
        val events: List<MidiEvent>,
        val totalTicks: Long
    )

    private object MidiParser {
        fun parse(file: File): MidiSequence {
            val raf = RandomAccessFile(file, "r")
            val events = mutableListOf<MidiEvent>()
            val header = ByteArray(4); raf.readFully(header)
            require(String(header) == "MThd") { "Not a MIDI file" }
            raf.readInt()
            val nTracks   = run { raf.readShort(); raf.readShort().toInt() and 0xFFFF }
            val division  = raf.readShort().toInt() and 0xFFFF
            val resolution = division and 0x7FFF
            for (t in 0 until nTracks) {
                val trkHeader = ByteArray(4); raf.readFully(trkHeader)
                if (String(trkHeader) != "MTrk") break
                val trkLen  = raf.readInt()
                val trkData = ByteArray(trkLen); raf.readFully(trkData)
                parseTrack(trkData, events)
            }
            raf.close()
            events.sortBy { it.tickAbsolute }
            val totalTicks = events.maxOfOrNull { it.tickAbsolute } ?: 0L
            return MidiSequence(resolution, events, totalTicks)
        }

        private fun parseTrack(data: ByteArray, out: MutableList<MidiEvent>) {
            var pos = 0; var tick = 0L; var runningStatus = 0
            fun readByte() = (data[pos++].toInt() and 0xFF)
            fun readVarLen(): Long {
                var value = 0L; var b: Int
                do { b = readByte(); value = (value shl 7) or (b and 0x7F).toLong() } while (b and 0x80 != 0)
                return value
            }
            while (pos < data.size) {
                tick += readVarLen()
                var status = readByte()
                if (status and 0x80 == 0) {
                    pos--; status = runningStatus
                }
                else if (status and 0xF0 != 0xF0) runningStatus = status

                val type = status and 0xF0;
                val ch = status and 0x0F
                when {
                    status == 0xFF -> {
                        val mt=readByte();
                        val len=readVarLen().toInt();
                        val md=ByteArray(len){data[pos++]};
                        out.add(MidiEvent(tick,0xFF,0,0,0,mt,md))
                    }
                    status == 0xF0 || status == 0xF7 -> repeat(readVarLen().toInt()) {
                        pos++
                    }
                    type == 0x80 -> {
                        val d1=readByte();
                        val d2=readByte();
                        out.add(MidiEvent(tick,0x80,ch,d1,d2))
                    }
                    type == 0x90 -> {
                        val d1=readByte();
                        val d2=readByte();
                        out.add(MidiEvent(tick,if(d2==0) 0x80 else 0x90,ch,d1,d2))
                    }
                    type == 0xA0 -> {
                        readByte(); readByte()
                    }
                    type == 0xB0 -> {
                        val d1=readByte();
                        val d2=readByte();
                        out.add(MidiEvent(tick,0xB0,ch,d1,d2))
                    }
                    type == 0xC0 -> {
                        val d1=readByte();
                        out.add(MidiEvent(tick,0xC0,ch,d1,0))
                    }
                    type == 0xD0 -> readByte()
                    type == 0xE0 -> {
                        readByte(); readByte()
                    }
                }
            }
        }
    }

    private val midiDriver: MidiDriver = MidiDriver.getInstance()
    private var sequence: MidiSequence? = null
    private var resolution: Int = 480
    @Volatile private var usPerTick: Double = (60_000_000.0 / 120.0) / 480.0

    @Volatile private var isRunning = false
    @Volatile private var isHolding = false
    private var currentTickPos: Long = 0L
    private var lastEventNano: Long = System.nanoTime()
    private var targetBpm: Float = 120f

    @Volatile private var currentPlaybackBpm: Float = 120f
    @Volatile private var isInTempoChange: Boolean = false
    @Volatile private var needsClockSync = false
    private var tempoChangeJob: Job? = null
    private val voiceVolumes = FloatArray(4) { 127f }
    private var currentGlobalVolume: Float = 0.8f
    private var currentDynamicFactor: Float = Dynamic.MF.factor

    private var pointA: Long = -1L
    private var pointB: Long = -1L
    private var isLoopingAB = false

    private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
    private var playJob: Job? = null
    private var navigationJob: Job? = null
    private var syncJob: Job? = null
    private var dynamicJob: Job? = null
    private var boundModel: SharedScreenModel? = null
    private val settings: Settings by inject()

    private data class NavigationStep(
        val marker: String,
        val gridIndex: Int,
        val targetGrid: Int = 0,
        val isHold: Boolean = false,
        var alreadyDone: Boolean = false,
        var beatInDC: Int = 1,
        val dynamic: Dynamic? = null,
        val hairPin: Char? = null,
        val hairPinEndGrid: Int = -1,
        val hairPinFromFactor: Float = 1.0f,
        val hairPinToFactor: Float = 1.0f,
        val isFarany: Boolean = false,
        var faranyActive: Boolean = false,
        // rit & rall
        val isTempoChange: Boolean = false,
        val tempoType: String = "",
        val endGridForTempo: Int = -1,
        val targetTempoMultiplier: Float = 0.6f
    )
    private val navigationSteps = mutableListOf<NavigationStep>()

    init {
        midiDriver.start()
        loadVoiceVolumes()
        loadSavedInstrumentsToPlayer(this)
        val file = File(filename)
        if (file.exists()) {
            try {
                sequence   = MidiParser.parse(file)
                resolution = sequence!!.resolution
                usPerTick  = (60_000_000.0 / targetBpm) / resolution
            } catch (e: Exception) { e.printStackTrace() }
        }
    }

    private fun send(vararg bytes: Int) {
        midiDriver.write(ByteArray(bytes.size) { bytes[it].toByte() })
    }
    private fun controlChange(ch: Int, cc: Int, v: Int) = send(0xB0 or ch, cc, v)
    private fun allNotesOff() {
        for (ch in 0 until 4) { controlChange(ch, 123, 0); controlChange(ch, 64, 0) }
    }

    private fun applyVoiceStates() {
        for (i in 0 until 4) {
            val vol = (127f * (voiceVolumes[i]/127f) * currentGlobalVolume * currentDynamicFactor)
                .toInt().coerceIn(0, 127)
            controlChange(i, 7, vol); controlChange(i, 11, vol)
            if (vol == 0) controlChange(i, 123, 0)
        }
    }
    private fun applyDynamic(dynamic: Dynamic) {
        applyDynamicSmooth(if (dynamic == Dynamic.MF) 1.0f else dynamic.factor, 300L)
    }
    private fun applyDynamicSmooth(targetFactor: Float, durationMs: Long = 300L) {
        dynamicJob?.cancel()
        dynamicJob = playerScope.launch {
            val start = currentDynamicFactor; val steps = 30; val stepMs = durationMs / steps
            for (i in 1..steps) {
                val p = i.toFloat()/steps; val s = p*p*(3f-2f*p)
                currentDynamicFactor = start + (targetFactor - start) * s
                applyVoiceStates(); delay(stepMs)
            }
            currentDynamicFactor = targetFactor; applyVoiceStates()
        }
    }
    private fun applyCrescendo(fromFactor: Float, toFactor: Float, durationMs: Long) {
        dynamicJob?.cancel()
        dynamicJob = playerScope.launch {
            val steps=40; val stepMs=(durationMs/steps).coerceAtLeast(10L)
            for (i in 1..steps) {
                val p=i.toFloat()/steps; val s=p*p*(3f-2f*p)
                currentDynamicFactor = fromFactor + (toFactor-fromFactor)*s
                applyVoiceStates(); delay(stepMs)
            }
            currentDynamicFactor = toFactor; applyVoiceStates()
        }
    }

    private fun applyTempoChange(
        startBpm: Float,
        endBpm: Float,
        durationMs: Long,
        tempoType: String
    ) {
        tempoChangeJob?.cancel()
        isInTempoChange = true
        tempoChangeJob = playerScope.launch {
            val steps     = 50
            val stepMs    = (durationMs / steps).coerceAtLeast(20L)
            val startTime = System.currentTimeMillis()

            for (i in 1..steps) {
                val elapsed  = System.currentTimeMillis() - startTime
                val progress = (elapsed.toFloat() / durationMs).coerceIn(0f, 1f)
                val smooth   = progress * progress * (3f - 2f * progress) // ease-in-out
                currentPlaybackBpm = startBpm + (endBpm - startBpm) * smooth
                usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution
                needsClockSync = true
                delay(stepMs)
                if (!isActive) break
            }

            currentPlaybackBpm = endBpm
            usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution
            isInTempoChange = false
            println("$tempoType terminé → $endBpm BPM")
        }
    }

    private fun resetTempoToNormal() {
        if (kotlin.math.abs(currentPlaybackBpm - targetBpm) > 0.5f) {
            applyTempoChange(
                startBpm   = currentPlaybackBpm,
                endBpm     = targetBpm,
                durationMs = 800L,
                tempoType  = "Retour tempo"
            )
        }
    }

    private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull {
        it.label == marker.trim().removeSuffix("=").trim()
    }
    fun factorToDynamic(factor: Float) =
        Dynamic.entries.minByOrNull { kotlin.math.abs(it.factor - factor) } ?: Dynamic.MF
    fun nextDynamic(currentFactor: Float, symbol: Char): Float {
        val idx = Dynamic.entries.indexOf(factorToDynamic(currentFactor))
        return if (symbol == '<') Dynamic.entries.getOrElse(idx+1){Dynamic.FFF}.factor
        else               Dynamic.entries.getOrElse(idx-1){Dynamic.PPP}.factor
    }

    private fun startPlaybackLoop() {
        val seq = sequence ?: return
        isRunning = true
        playJob?.cancel()
        playJob = playerScope.launch(Dispatchers.Default) {
            val events = seq.events
            var idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
                .takeIf { it >= 0 } ?: events.size
            var clockNano = System.nanoTime()
            var clockTick = currentTickPos

            while (isActive && isRunning) {
                if (needsClockSync) {
                    clockNano = System.nanoTime()
                    clockTick = currentTickPos
                    idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
                        .takeIf { it >= 0 } ?: events.size

                    needsClockSync = false
                }
                if (isHolding) {
                    yield()
//                    delay(10)
                    clockNano = System.nanoTime()
                    clockTick = currentTickPos
                    continue
                }

                if (idx >= events.size) {
                    val dc = getPendingDcStep()
                    if (dc != null) {
                        allNotesOff()
                        seekToGrid(dc.targetGrid)
                        clockNano = System.nanoTime(); clockTick = currentTickPos
                        idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
                            .takeIf { it >= 0 } ?: events.size
                        continue
                    } else {
                        isRunning = false
                        allNotesOff()
                        onFinished()
                        break
                    }
                }

                val ev = events[idx]
//                val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000)
//                    .toLong()) - System.nanoTime()

                val targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong()
                val now = System.nanoTime()
                val waitNano = targetNano - now

//                if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L))
                if (waitNano > 0) {
                    // Thread.sleep est beaucoup plus précis que delay() pour les micro-délais MIDI
                    val ms = waitNano / 1_000_000
                    val ns = (waitNano % 1_000_000).toInt()
                    try {
                        Thread.sleep(ms, ns)
                    } catch (e: Exception) {
                        yield()
                    }
                }
                if (needsClockSync) {
                    continue
                }

                currentTickPos = ev.tickAbsolute
                lastEventNano  = System.nanoTime()

                // A-B loop
                if (isLoopingAB && pointA >= 0 && pointB > pointA &&
                    ticksToMs(currentTickPos) >= pointB) {
                    allNotesOff(); currentTickPos = msToTicks(pointA)
                    clockNano = System.nanoTime(); clockTick = currentTickPos
                    idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
                        .takeIf { it >= 0 } ?: events.size
                    continue
                }

                when (ev.type) {
                    0x80 -> send(0x80 or ev.channel, ev.data1, ev.data2)
                    0x90 -> send(0x90 or ev.channel, ev.data1,
                        (ev.data2 * currentDynamicFactor).toInt().coerceIn(0, 127))
                    0xB0 -> if (ev.data1 != 7 && ev.data1 != 11)
                        send(0xB0 or ev.channel, ev.data1, ev.data2)
                    0xC0 -> send(0xC0 or ev.channel, ev.data1)
                    0xFF -> { /* tempo ignoré */ }
                }
                val currentGrid = (currentTickPos/* ev.tickAbsolute*/ / resolution).toLong()
                if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
                    boundModel?.updateActiveIndex(currentGrid)
                }
                idx++
            }
        }
    }

    actual fun seekToGrid(gridIndex: Int) {
        currentTickPos = gridIndex.toLong() * resolution
        lastEventNano  = System.nanoTime()

        val lastDyn = navigationSteps
            .filter { it.dynamic != null && it.gridIndex <= gridIndex }
            .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
        currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor
        applyVoiceStates()

        needsClockSync = true
    }
    private fun ticksToMs(ticks: Long) = (ticks * usPerTick / 1000.0).toLong()
    private fun msToTicks(ms: Long)    = (ms * 1000.0 / usPerTick).toLong()

    private fun getPendingDcStep(): NavigationStep? =
        navigationSteps.firstOrNull {
            !it.alreadyDone &&
                    it.hairPin == null &&
                    it.dynamic == null &&
                    !it.isHold &&
                    !it.isFarany
        }

    private fun resetNavigationFlags() {
        navigationSteps.forEach { it.alreadyDone = false }
    }

    private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
        val metadataList = sharedScreenModel.getFullMarkers()
        navigationSteps.clear()
        var lastSegno=0;
        var lastFactor=Dynamic.MF.factor
        val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
        val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
        val dsR       = Regex("""D\.?S\.?""");
        val dcR       = Regex("""D\.?C\.?""")
        /*val dsG       = Regex("""D\.?S\.?_GROUP_PART""");
        val dcG       = Regex("""D\.?C\.?_GROUP_PART""")*/
        val finReg    = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE)

        metadataList.forEach { midiMarker ->
            val ci        = midiMarker.gridIndex ?: 0
            val last_grid = sharedScreenModel.getTotalGridCount()
            val hairPins  = sharedScreenModel.getHairPins()
            val markerList = midiMarker.marker

            var i = 0
            while(i < markerList.size) {
                val marker = markerList[i]
                val mTrim = marker.trim()
                val nextMarker = markerList.getOrNull(i + 1)
                val nextTrim = nextMarker?.trim() ?: ""

                val isDsFin = (dsR.containsMatchIn(mTrim) && finReg.containsMatchIn(nextTrim)) ||
                        (dsR.containsMatchIn(nextTrim) && finReg.containsMatchIn(mTrim))
                val isDcFin = (dcR.containsMatchIn(mTrim) && finReg.containsMatchIn(nextTrim)) ||
                        (dcR.containsMatchIn(nextTrim) && finReg.containsMatchIn(mTrim))
                when {
                    isDsFin || isDcFin -> {
                        val target = if (lastSegno > 0) lastSegno else 0
                        navigationSteps.add(NavigationStep(
                            marker = "$mTrim$nextTrim",
                            gridIndex = ci,
                            targetGrid = if (isDsFin) target else 0
                        ))
                        i += 2
                        continue
                    }

                    marker.contains("$") -> lastSegno = ci

                    marker.contains("\uD834\uDD10") -> {
                        val beat = if(midiMarker.note.contains('•')) 2 else 1
                        navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat))
                    }
                    ritRegex.containsMatchIn(marker) -> {
                        val endGrid = if(midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker
                        navigationSteps.add(
                            NavigationStep(
                                marker = marker,
                                gridIndex = ci,
                                isTempoChange = true,
                                tempoType = "rit",
                                endGridForTempo = endGrid,
                                targetTempoMultiplier = 0.55f
                            )
                        )
                    }
                    rallRegex.containsMatchIn(marker) -> {
                        val endGrid = last_grid
                        navigationSteps.add(
                            NavigationStep(
                                marker = marker,
                                gridIndex = ci,
                                isTempoChange = true,
                                tempoType = "rall",
                                endGridForTempo = endGrid,
                                targetTempoMultiplier = 0.50f
                            )
                        )
                    }

                    dsR.matches(mTrim) -> {
                        navigationSteps.add(NavigationStep(marker, ci,
                            targetGrid = if(lastSegno>0) lastSegno else 0))
                    }
                    dcR.matches(mTrim) -> {
                        val indx = if((last_grid-ci)<=0) ci else ci
                        navigationSteps.add(NavigationStep(marker, indx, targetGrid=0))
                        println("DC créé à $indx → 0")
                    }

                    // ── Farany ────────────────────────────────
                    finReg.containsMatchIn(mTrim) -> {
                        val hasDcAfter = metadataList.any { m ->
                            (m.gridIndex ?: 0) > ci && m.marker.any { mk -> dcR.containsMatchIn(mk) }
                        }
                        if (hasDcAfter) {
                            navigationSteps.add(
                                NavigationStep(
                                    marker,
                                    ci,
                                    targetGrid = last_grid,
                                    isFarany = true, faranyActive = false)
                            )
                        } else {
                            val verse = sharedScreenModel.stanza.value
                            val verses = sharedScreenModel.nbStanzas.value
                            println("Farany ignoré à $ci i $verse == $verses")
                            navigationSteps.add(
                                NavigationStep(
                                    marker = "${marker}_STZ",
                                    gridIndex = ci,
                                    isFarany = true, faranyActive = (verse != verses)
                                )
                            )
                        }
                    }

                    extractDynamic(marker) != null && mTrim != "=" -> {
                        val dyn = extractDynamic(marker) ?: return@forEach
                        lastFactor = if(dyn==Dynamic.MF) 1.0f else dyn.factor
                        navigationSteps.add(NavigationStep(marker=marker, gridIndex=ci, dynamic=dyn))
                    }

                    mTrim=="<" || mTrim==">" ||
                            mTrim.contains("cres", ignoreCase=true) ||
                            mTrim.contains("dim",  ignoreCase=true) -> {
                        val sym = when {
                            mTrim=="<" -> '<'; mTrim==">" -> '>'
                            mTrim.contains("cres",ignoreCase=true) -> '<'; else -> '>'
                        }
                        val pair = hairPins.find{it.startGrid==ci} ?: return@forEach
                        val explicitAfter = metadataList
                            .filter{ m ->(m.gridIndex ?:0) >= pair.endGrid }
                            .mapNotNull { m ->
                                val dynamic = m.marker.firstNotNullOfOrNull { mk -> extractDynamic(mk) }
                                if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null
                            }
                            .minByOrNull { it.first }
                            ?.second

                        val from = lastFactor
                        val to = when {
                            explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor
                            explicitAfter!=null && sym=='>' && explicitAfter.factor<from -> explicitAfter.factor
                            else -> nextDynamic(from, sym)
                        }
                        lastFactor = to
                        navigationSteps.add(NavigationStep(
                            marker=mTrim, gridIndex=ci, hairPin=sym,
                            hairPinEndGrid=pair.endGrid,
                            hairPinFromFactor=from, hairPinToFactor=to
                        ))
                    }
                }
                i++
            }
        }
    }

    private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
        var lastProcessedIndex = -1
        var isJumping = false
        var pendingFin = false
        navigationJob?.cancel()

        navigationJob = playerScope.launch(Dispatchers.Default) {
            sharedScreenModel.activeIndex.collect { currentIndex ->
                //if (currentIndex <= lastProcessedIndex && lastProcessedIndex != -1) return@collect

                lastProcessedIndex = currentIndex

                if (!isRunning || currentIndex < 0) return@collect
                val currentSteps = navigationSteps.filter {
                    it.gridIndex == currentIndex && !it.alreadyDone
                }.sortedBy { step ->
                    when {
                        step.isHold -> 0
                        step.isTempoChange -> 1
                        step.isFarany -> 1
                        step.dynamic != null || step.hairPin != null -> 1
                        else -> 3
                    }
                }
                for(step in currentSteps) {
                    when {
                        // ── Point d'orgue ─────────────────────
                        step.isHold -> {
                                val beatMs = (60_000 / targetBpm).toLong()
                                val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2

                                isHolding = true
                                for (ch in 0 until 4) controlChange(ch, 64, 127)
                                delay(holdDuration)
                                for (ch in 0 until 4) controlChange(ch, 64, 0)
                                isHolding = false
                                println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms")
                            }
                        //  Rit. / Rall. , ...
                        step.isTempoChange -> {
                            step.alreadyDone = true
                            val startBpm      = currentPlaybackBpm
                            val endBpm        = targetBpm * step.targetTempoMultiplier
                            val gridDistance  = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1)
                            val beatDurationMs = (60_000L / targetBpm).toLong()
                            val durationMs    = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L)

                            println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}")
                            println("  $startBpm BPM → $endBpm BPM sur ${durationMs}ms")

                            applyTempoChange(
                                startBpm   = startBpm,
                                endBpm     = endBpm,
                                durationMs = durationMs,
                                tempoType  = step.tempoType
                            )
                        }

                        // ── Soufflet ──────────────────────────
                        step.hairPin != null -> {
                            step.alreadyDone = true
                            val dist   = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1)
                            val beatMs = (60_000L / targetBpm).toLong()
                            applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor,
                                (dist * beatMs).coerceIn(200L, 5000L))
                        }

                        // ── Dynamique ─────────────────────────
                        step.dynamic != null -> {
                            step.alreadyDone = true
                            applyDynamic(step.dynamic)
                        }

                        // ── Farany ─────────────────────────
                        step.isFarany -> {
                            println("C  Farany ok $step")
                            if (pendingFin || step.faranyActive) {
                                println("Farany activé → STOP à grille $currentIndex")
                                isRunning  = false
                                allNotesOff()
                                onFinished()
                                return@collect
                            } else {
                                println("Farany 1er passage — DC pas encore vu")
                                pendingFin = true
                                step.alreadyDone = true
                                continue
                            }
                        }
                    // ── DC / DS ───────────────────────────
                    else -> {
                        step.alreadyDone = true
                        val beatMs = (60_000 / targetBpm).toLong()
                        println("avant de sauter bpm=$targetBpm")

                        //lastProcessedIndex = -1
                        for (ch in 0 until 4) controlChange(ch, 64, 127)
                        isHolding = true
                        delay(beatMs)
                        allNotesOff()

                        seekToGrid(step.targetGrid)
                        isHolding = false
                        for (ch in 0 until 4) controlChange(ch, 64, 0)

                        navigationSteps
                            .filter { it.isFarany && step.gridIndex > it.gridIndex }
                            .forEach {
                                it.faranyActive = true
                                it.alreadyDone  = false
                                println("DC grille ${step.gridIndex} > Farany grille ${it.gridIndex} → Farany activé 🔴")
                            }

                        startPlaybackLoop()
                        }
                    }
                }
            }
        }
    }

    private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
        syncJob?.cancel()
        syncJob = playerScope.launch(Dispatchers.Default) {
            while (isActive) {
                if (isRunning && !isHolding) {
                    val elapsedNano  = System.nanoTime() - lastEventNano
                    val extraTicks   = (elapsedNano / (usPerTick * 1000)).toLong().coerceAtLeast(0L)
                    val interpolated = (currentTickPos + extraTicks) / resolution
                    val rawGrid      = interpolated.toInt().coerceAtLeast(0)
                    sharedScreenModel.updateActiveIndexByIndex(rawGrid)
                }
                delay(16)
            }
        }
    }

    private var countInJob: Job? = null

    private fun getCountInBeats(measureStr: String): Int {
        val firstNumeratorChar = measureStr.substringBefore('/').trim()
        return firstNumeratorChar.toIntOrNull() ?: 4
    }
    actual fun play() {
        if (sequence == null) return
        if(currentTickPos == 0L) {
            playCountIn()
        } else {
            applyVoiceStates()
            startPlaybackLoop()
            println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm")
        }
    }

    private fun playCountIn() {
        countInJob?.cancel()
        countInJob = playerScope.launch(Dispatchers.Default) {
            val bpm = targetBpm
            val beatDurationMs = (60_000L / bpm).toLong()

            val noteEvent = 42 // 39:Hand clap
            val velocity = 100
            val originalMeasure = sharedScreenModel.measureInt.value
            val nbSilent = sharedScreenModel.silentDurationBefore.value

            val adjustedMeasure = when {
                originalMeasure == 6 -> 3
                originalMeasure % 2 != 0 && originalMeasure > 3 -> 3
                originalMeasure % 2 == 0 && originalMeasure > 4 -> 4
                else -> originalMeasure
            }
            val anacrouseBeats = nbSilent / 4
            val noteOnEvent = byteArrayOf(0x99.toByte(), noteEvent.toByte(), velocity.toByte())
            val noteOffEvent = byteArrayOf(0x89.toByte(), noteEvent.toByte(), 0.toByte())

            val noteOnSnare = byteArrayOf(0x99.toByte(), 38.toByte(), velocity.toByte())
            val noteOffSnare = byteArrayOf(0x89.toByte(), 38.toByte(), 0.toByte())

            val isEvenMeasure = adjustedMeasure in setOf(2, 4, 8)
            if (isEvenMeasure) {
                for (i in 1..2) {
                    midiDriver.write(noteOnEvent)
                    delay(100)
                    midiDriver.write(noteOffEvent)

                    val slowDuration = beatDurationMs * 2
                    delay((slowDuration - 100).coerceAtLeast(0L))
                }

                val remainingBeats = if (anacrouseBeats == 0) 4 else anacrouseBeats
                for (i in 1..remainingBeats) {
                    val onBytes = if (i == remainingBeats) noteOnSnare else noteOnEvent
                    val offBytes = if (i == remainingBeats) noteOffSnare else noteOffEvent

                    midiDriver.write(onBytes)
                    delay(100)
                    midiDriver.write(offBytes)

                    delay((beatDurationMs - 100).coerceAtLeast(0L))
                }

            } else {
                val totalBeats = if (anacrouseBeats > 5) anacrouseBeats else adjustedMeasure + anacrouseBeats
//            println("Android dcompte total : $totalBeats bips ($adjustedMeasure de préparation + $anacrouseBeats d'anacrouse) à $bpm BPM")

                for (i in 1..totalBeats) {
                    val onBytes = if (i == adjustedMeasure) noteOnSnare else noteOnEvent
                    val offBytes = if (i == adjustedMeasure) noteOffSnare else noteOffEvent

                    midiDriver.write(onBytes)
                    delay(100)
                    midiDriver.write(offBytes)

                    delay((beatDurationMs - 100).coerceAtLeast(0L))
                }
            }

            withContext(Dispatchers.Main) {
                applyVoiceStates()
                startPlaybackLoop()
            }
        }
    }
    actual fun pause() {
        countInJob?.cancel()
        isRunning = false; isHolding = false
        playJob?.cancel(); allNotesOff()
    }
    actual fun stop() {
        countInJob?.cancel()
        isRunning = false; isHolding = false
        playJob?.cancel(); navigationJob?.cancel()
        allNotesOff(); currentTickPos = 0L
        resetNavigationFlags(); navigationSteps.clear(); clearLoop()
        currentDynamicFactor = Dynamic.MF.factor
        resetTempoToNormal()
    }
    actual fun release() {
        stop(); midiDriver.stop(); playerScope.cancel()
    }
    actual fun seekTo(position: Long) {
        currentTickPos = msToTicks(position)
        lastEventNano  = System.nanoTime()
        applyVoiceStates()
    }
    actual fun getDuration(): Long = sequence?.let { ticksToMs(it.totalTicks) } ?: 0L
    actual fun getCurrentPosition(): Long = ticksToMs(currentTickPos)
    actual fun setVolume(level: Float) {
        currentGlobalVolume = level
        midiDriver.setVolume((level*100).toInt().coerceIn(0,100))
        applyVoiceStates()
    }
    actual fun setTempo(bpm: Float) {
        targetBpm = bpm; usPerTick = (60_000_000.0/bpm)/resolution
        boundModel?.let { prepareNavigation(it) }
        println("Tempo → $bpm BPM")
    }
    actual fun getCurrentBPM(): Float = targetBpm
    actual fun requestSync(sharedScreenModel: SharedScreenModel) {
        val seq = sequence ?: return
        val totalGrids = (seq.totalTicks/resolution).toInt()
        val timestamps = (0..totalGrids).map { ticksToMs(it.toLong()*resolution)*1000L }
        sharedScreenModel.updateTimestamps(timestamps)
        println("requestSync Android — $totalGrids grilles")
    }
    actual fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel) {
        this.boundModel = sharedScreenModel
        prepareNavigation(sharedScreenModel)
        startNavigationMonitor(sharedScreenModel)
        startSyncLoop(sharedScreenModel)
    }
    actual fun setPointA() { pointA = getCurrentPosition() }
    actual fun setPointB() {
        pointB = getCurrentPosition()
        if (pointB > pointA && pointA != -1L) isLoopingAB = true
    }
    actual fun clearLoop() { isLoopingAB = false; pointA = -1L; pointB = -1L }
    actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB)
    actual fun toggleVoice(index: Int) { applyVoiceStates() }
    private fun saveVoicesVolumes() {
        val data = voiceVolumes.joinToString(",")
        settings.putString("voices_volumes", data)
    }

    private fun loadVoiceVolumes() {
        val data = settings.getString("voices_volumes", "127,127,127,127")
        val volumesArray = data.split(",")

        if (volumesArray.size == 4) {
            for (i in 0 until 4) {
                voiceVolumes[i] = volumesArray[i].toFloatOrNull() ?: 127f
            }
        }
    }
    actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) {
        if (voiceIndex in 0..3) {
            voiceVolumes[voiceIndex] = newVolume;
            saveVoicesVolumes()
            applyVoiceStates()
        }
    }
    actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
    actual fun changeInstru(noInstru: Int) {
        for (ch in 0 until 4) send(0xC0 or ch, noInstru)
    }
    actual fun getAvalaibleInstruments(): List<MidiInstrument> {
        val gmInstruments = listOf(
            "Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano", "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavi", "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", "Xylophone", "Tubular Bells", "Dulcimer", "Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ", "Reed Organ", "Accordion", "Harmonica", "Tango Accordion", "Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)", "Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Guitar harmonics", "Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani", "String Ensemble 1", "String Ensemble 2", "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", "Brass Section", "SynthBrass 1", "SynthBrass 2", "Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet", "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Shakuhachi", "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass+lead)", "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)", "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bag pipe", "Fiddle", "Shanai", "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", "Applause", "Gunshot"
        )

        return gmInstruments.mapIndexed { index, name ->
            MidiInstrument(
                program = index,
                name = name
            )
        }
    }

    actual fun saveVoiceInstrument(program: Int) {
        settings.putInt("voice_instrument", program)
    }

    actual fun getVoiceInstrument(): Int {
        val defaultProgram = 0
        return settings.getInt("voice_instrument", defaultProgram)
    }

    fun loadSavedInstrumentsToPlayer(mediaPlayer: FMediaPlayer) {
        val savedProgram = getVoiceInstrument()
        mediaPlayer.changeInstru(savedProgram)
    }
}