package mg.dot.feufaro.midi

import SharedScreenModel
import kotlinx.coroutines.*
import org.billthefarmer.mididriver.MidiDriver
import java.io.File
import java.io.RandomAccessFile

actual class FMediaPlayer actual constructor(
    private val filename: String,
    private val onFinished: () -> Unit
) {
    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 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()
        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
                    needsClockSync = false
                }
                if (isHolding) {
                    yield()
//                    delay(10)
                    clockNano = System.nanoTime()
                    clockTick = currentTickPos
                    continue
                }

                if (idx >= events.size) {
                    val dc = getPendingDcStep()
                    if (dc != null) {
                        dc.alreadyDone = true
                        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()
                    }
                }
                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 = (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()  // ← recaler l'horloge d'interpolation
        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()
    }
    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)

        metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
            val ci        = gridIndex ?: 0
            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 last_grid = sharedScreenModel.getTotalGridCount()
            val hairPins  = sharedScreenModel.getHairPins()

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

                marker.contains("\uD834\uDD10") -> {
                    val beat = if(note.contains('•')) 2 else 1
                    navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat))
                }
                ritRegex.containsMatchIn(marker) -> {
                    val endGrid = if(lastCallerMarker == 0) last_grid else 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
                        )
                    )
                }
                dsG.matches(marker.trim()) -> {
                    val target = if(lastSegno>0) lastSegno else 0
                    val indx   = if((last_grid-ci)<=0) ci-1 else ci
                    navigationSteps.add(NavigationStep(marker, indx, targetGrid=target))
                }

                dsR.matches(marker.trim()) || marker == "DSFin" -> {
                    navigationSteps.add(NavigationStep(marker, ci,
                        targetGrid = if(lastSegno>0) lastSegno else 0))
                }

                dcG.matches(marker.trim()) -> {
                    val indx = if((last_grid-ci)<=0) ci-1 else ci
                    navigationSteps.add(NavigationStep(marker, indx, targetGrid=0))
                    println("DC_GROUP créé à $indx → 0")
                }

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

                // ── Farany ────────────────────────────────
                marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> {
                    val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) ->
                        (gi ?: 0) > ci &&
                                (mk.contains(Regex("""D\.?C\.?""")) || mk.contains("DC_GROUP_PART"))
                    }
                    if (hasDcAfter) {
                        navigationSteps.add(NavigationStep(
                            marker       = marker,
                            gridIndex    = ci,
                            targetGrid   = last_grid,
                            isFarany     = true,
                            faranyActive = false
                        ))
                        println("Farany mémorisé à $ci")
                    } else {
                        println("Farany ignoré à $ci")
                    }
                }

                extractDynamic(marker) != null && marker.trim() != "=" -> {
                    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))
                }

                marker.trim()=="<" || marker.trim()==">" ||
                        marker.trim().contains("cres", ignoreCase=true) ||
                        marker.trim().contains("dim",  ignoreCase=true) -> {
                    val sym = when {
                        marker.trim()=="<" -> '<'; marker.trim()==">" -> '>'
                        marker.trim().contains("cres",ignoreCase=true) -> '<'; else -> '>'
                    }
                    val pair = hairPins.find{it.startGrid==ci} ?: return@forEach
                    val explicitAfter = metadataList
                        .filter{(_,gi,_,_,mk,_,_,_)->(gi?:0)>=pair.endGrid && extractDynamic(mk)!=null}
                        .minByOrNull{it.gridIndex?:0}?.let{m->extractDynamic(m.marker)}
                    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=marker.trim(), gridIndex=ci, hairPin=sym,
                        hairPinEndGrid=pair.endGrid,
                        hairPinFromFactor=from, hairPinToFactor=to
                    ))
                }
            }
        }
    }

    private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
        var lastProcessedIndex = -1
        navigationJob?.cancel()
        navigationJob = playerScope.launch(Dispatchers.Default) {
            sharedScreenModel.activeIndex.collect { currentIndex ->
                if (currentIndex <= lastProcessedIndex) {
                    return@collect
                }
                lastProcessedIndex = currentIndex
                if (!isRunning || currentIndex < 0) return@collect

                val step = navigationSteps.find {
                    it.gridIndex == currentIndex && !it.alreadyDone
                }
                println("pas: ${currentIndex}")
                if (step != null) {
                    when {
                    // ── Farany ────────────────────────────
                    step.isFarany -> {
                        if (step.faranyActive) {
                            println("Farany activé → STOP à grille $currentIndex")
                            step.alreadyDone = true
                            isHolding = false
                            isRunning  = false
                            playJob?.cancel()
                            allNotesOff()
                            onFinished()
                        } else {
                            println("Farany 1er passage — DC pas encore vu → on continue ✅")
                        }
                    }

                    // ── Point d'orgue ─────────────────────
                    step.isHold -> {
                        val beatMs       = (60_000 / targetBpm).toLong()
                        val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2
                        println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms")
                        for (ch in 0 until 4) controlChange(ch, 64, 127)
                        isHolding = true
                        delay(holdDuration)
                        isHolding = false
                        for (ch in 0 until 4) controlChange(ch, 64, 0)
                        step.alreadyDone = true
                    }
                    //  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)
                    }

                    // ── DC / DS ───────────────────────────
                    else -> {
                        step.alreadyDone = true
                        val beatMs = (60_000 / targetBpm).toLong()
                        println("avant de sauter bpm=$targetBpm")

                        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()
                        println("DC/DS → grille ${step.targetGrid}")
                    }
                }
                }
            }
        }
    }

    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)
            }
        }
    }

    actual fun play() {
        if (sequence == null) return
        applyVoiceStates()
        startPlaybackLoop()
        println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm")
    }
    actual fun pause() {
        isRunning = false; isHolding = false
        playJob?.cancel(); allNotesOff()
    }
    actual fun stop() {
        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() }
    actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) {
        if (voiceIndex in 0..3) { voiceVolumes[voiceIndex] = newVolume; applyVoiceStates() }
    }
    actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
    actual fun changeInstru(noInstru: Int) {
        for (ch in 0 until 4) send(0xC0 or ch, noInstru)
    }
}