package dev.atsushieno.ktmidi

class Midi2Machine {
    fun interface Listener {
        fun onEvent(e: Ump)
    }

    var diagnosticsHandler: (String, Ump?) -> Unit =
        { message, ump -> throw UnsupportedOperationException(message + (if (ump != null) " : $ump" else null)) }

    val eventListeners by lazy { mutableListOf<Listener>() }

    @Deprecated("directly use eventListeners property")
    fun addListener(listener: Listener) {
        eventListeners.add(listener)
    }

    @Deprecated("directly use eventListeners property")
    fun removeListener(listener: Listener) {
        eventListeners.remove(listener)
    }

    var systemCommon = Midi1SystemCommon() // They are compatible with MIDI2 protocol

    private val channels = mutableMapOf<Int,Midi2MachineChannel>()
    val usedChannels : Iterable<Midi2MachineChannel>
        get() = channels.values
    fun channel(index: Int): Midi2MachineChannel {
        var ch = channels[index]
        if (ch == null) {
            ch = Midi2MachineChannel()
            channels[index] = ch
        }
        return ch
    }

    private fun withNoteRangeCheckV1(u: Ump, action: () -> Unit) = if (u.midi1Msb in 0..127) action() else diagnosticsHandler("Note is out of range", u)
    private fun withNoteRangeCheckV2(u: Ump, action: () -> Unit) = if (u.midi2Note in 0..127) action() else diagnosticsHandler("Note is out of range", u)

    fun processEvent(evt: Ump) {
        when (evt.messageType) {
            MidiMessageType.MIDI1 -> {
                when (evt.statusCode) {
                    MidiChannelStatus.NOTE_ON ->
                        withNoteRangeCheckV1(evt) {
                            with(channel(evt.groupAndChannel)) {
                                noteVelocity[evt.midi1Msb] = (evt.midi1Lsb shl 9).toUShort()
                                noteOnStatus[evt.midi1Msb] = true
                            }
                        }
                    MidiChannelStatus.NOTE_OFF ->
                        withNoteRangeCheckV1(evt) {
                            with(channel(evt.groupAndChannel)) {
                                noteVelocity[evt.midi1Msb] = (evt.midi1Lsb shl 9).toUShort()
                                noteOnStatus[evt.midi1Msb] = false
                            }
                        }
                    MidiChannelStatus.PAF ->
                        withNoteRangeCheckV1(evt) {
                            channel(evt.groupAndChannel).pafVelocity[evt.midi1Msb] = (evt.midi1Lsb shl 25).toUInt()
                        }
                    MidiChannelStatus.CC -> {
                        // FIXME: handle RPNs and NRPNs by DTE
                        with(channel(evt.groupAndChannel)) {
                            when (evt.midi1Msb) {
                                MidiCC.NRPN_MSB,
                                MidiCC.NRPN_LSB ->
                                    dteTarget = DteTarget.NRPN
                                MidiCC.RPN_MSB,
                                MidiCC.RPN_LSB ->
                                    dteTarget = DteTarget.RPN

                                MidiCC.DTE_MSB ->
                                    processMidi1Dte(evt.midi1Lsb.toByte(), true)
                                MidiCC.DTE_LSB ->
                                    processMidi1Dte(evt.midi1Lsb.toByte(), false)
                                MidiCC.DTE_INCREMENT ->
                                    processMidi1DteIncrement()
                                MidiCC.DTE_DECREMENT ->
                                    processMidi1DteDecrement()
                            }
                            controls[evt.midi1Msb] = (evt.midi1Lsb shl 25).toUInt()
                            when (evt.midi2CCIndex) {
                                MidiCC.OMNI_MODE_OFF -> omniMode = false
                                MidiCC.OMNI_MODE_ON -> omniMode = true
                                MidiCC.MONO_MODE_ON -> monoPolyMode = false
                                MidiCC.POLY_MODE_ON -> monoPolyMode = true
                            }
                        }
                    }
                    MidiChannelStatus.PROGRAM ->
                        channel(evt.groupAndChannel).program = evt.midi1Msb.toByte()
                    MidiChannelStatus.CAF ->
                        channel(evt.groupAndChannel).caf = (evt.midi1Msb shl 25).toUInt()
                    MidiChannelStatus.PITCH_BEND ->
                        channel(evt.groupAndChannel).pitchbend = ((evt.midi1Msb.toUnsigned() shl 25) + (evt.midi1Lsb shl 18)).toUInt()
                }
            }
            MidiMessageType.MIDI2 -> {
                when (evt.statusCode) {
                    MidiChannelStatus.NOTE_ON ->
                        withNoteRangeCheckV2(evt) {
                            with(channel(evt.groupAndChannel)) {
                                noteOnStatus[evt.midi2Note] = true
                                noteVelocity[evt.midi2Note] = evt.midi2Velocity16.toUShort()
                                noteAttribute[evt.midi2Note] = evt.midi2NoteAttributeData.toUShort()
                                noteAttributeType[evt.midi2Note] = evt.midi2NoteAttributeType.toUShort()
                            }
                        }
                    MidiChannelStatus.NOTE_OFF ->
                        withNoteRangeCheckV2(evt) {
                            with(channel(evt.groupAndChannel)) {
                                noteOnStatus[evt.midi2Note] = false
                                noteVelocity[evt.midi2Note] = 0u
                            }
                        }
                    MidiChannelStatus.PAF ->
                        withNoteRangeCheckV2(evt) {
                            channel(evt.groupAndChannel).pafVelocity[evt.midi2Note] = evt.midi2PAfData
                        }
                    MidiChannelStatus.CC -> {
                        with(channel(evt.groupAndChannel)) {
                            controls[evt.midi2CCIndex] = evt.midi2CCData
                            when (evt.midi2CCIndex) {
                                MidiCC.OMNI_MODE_OFF -> omniMode = false
                                MidiCC.OMNI_MODE_ON -> omniMode = true
                                MidiCC.MONO_MODE_ON -> monoPolyMode = false
                                MidiCC.POLY_MODE_ON -> monoPolyMode = true
                            }
                        }
                    }
                    MidiChannelStatus.PROGRAM -> {
                        if (evt.midi2ProgramOptions and 1 != 0) {
                            channel(evt.groupAndChannel).controls[MidiCC.BANK_SELECT] =
                                evt.midi2ProgramBankMsb.toUInt()
                            channel(evt.groupAndChannel).controls[MidiCC.BANK_SELECT_LSB] =
                                evt.midi2ProgramBankMsb.toUInt()
                        }
                        channel(evt.groupAndChannel).program = evt.midi2ProgramProgram.toByte()
                    }
                    MidiChannelStatus.CAF ->
                        channel(evt.groupAndChannel).caf = evt.midi2CAfData
                    MidiChannelStatus.PITCH_BEND ->
                        channel(evt.groupAndChannel).pitchbend = evt.midi2PitchBendData
                    MidiChannelStatus.PER_NOTE_PITCH_BEND ->
                        channel(evt.groupAndChannel).perNotePitchbend[evt.midi2Note] = evt.midi2PitchBendData
                    MidiChannelStatus.PER_NOTE_RCC ->
                        withNoteRangeCheckV2(evt) {
                            channel(evt.groupAndChannel).perNoteRCC[evt.midi2PerNoteRCCIndex][evt.midi2Note] =
                                evt.midi2PerNoteRCCData
                        }
                    MidiChannelStatus.PER_NOTE_ACC ->
                        withNoteRangeCheckV2(evt) {
                            channel(evt.groupAndChannel).perNoteACC[evt.midi2PerNoteACCIndex][evt.midi2Note] =
                                evt.midi2PerNoteACCData
                        }
                    MidiChannelStatus.RPN ->
                        channel(evt.groupAndChannel).rpns[evt.midi2RpnMsb * 128 + evt.midi2RpnLsb] = evt.midi2RpnData
                    MidiChannelStatus.NRPN ->
                        channel(evt.groupAndChannel).nrpns[evt.midi2NrpnMsb * 128 + evt.midi2NrpnLsb] =
                            evt.midi2NrpnData
                    MidiChannelStatus.RELATIVE_RPN ->
                        channel(evt.groupAndChannel).rpns[evt.midi2RpnMsb * 128 + evt.midi2RpnLsb] =
                            (channel(evt.groupAndChannel).rpns[evt.midi2RpnMsb * 128 + evt.midi2RpnLsb].toLong() + evt.midi2RpnData.toInt()).toUInt()
                    MidiChannelStatus.RELATIVE_NRPN ->
                        channel(evt.groupAndChannel).nrpns[evt.midi2RpnMsb * 128 + evt.midi2RpnLsb] =
                            (channel(evt.groupAndChannel).nrpns[evt.midi2NrpnMsb * 128 + evt.midi2NrpnLsb].toLong() + evt.midi2NrpnData.toInt()).toUInt()
                }
            }
        }
        for (receiver in eventListeners)
            receiver.onEvent(evt)
    }
}

class Midi2MachineChannel {
    val noteOnStatus = Array<Boolean>(128) { false }
    val noteVelocity = Array<UShort>(128) { 0u }
    val noteAttribute = Array<UShort>(128) { 0u }
    val noteAttributeType = Array<UShort>(128) { 0u }
    val pafVelocity = Array<UInt>(128) { 0u }
    val controls = Array<UInt>(128) { 0u }
    // They need independent flag to indicate which was set currently.
    var omniMode: Boolean? = null
    var monoPolyMode: Boolean? = null
    val perNoteRCC = Array(128) { Array<UInt>(128) { 0u } }
    val perNoteACC = Array(128) { Array<UInt>(128) { 0u } }
    val rpns = Array<UInt>(128 * 128) { 0u } // only 5 should be used though...
    val nrpns = Array<UInt>(128 * 128) { 0u }
    var program: Byte = 0
    var caf: UInt = 0u
    var pitchbend: UInt = 0x80000000u
    val perNotePitchbend = Array<UInt>(128) { 0x80000000u }
    var dteTarget: DteTarget = DteTarget.RPN
    private var dte_target_value: Byte = 0

    // This SHOULD NOT HAPPEN, but in case they were sent as legacy MIDI 1.0 messages...
    fun processMidi1Dte(value: Byte, isMsb: Boolean) {
        var arr: Array<UInt>
        when (dteTarget) {
            DteTarget.RPN -> {
                dte_target_value = (controls[(if (isMsb) MidiCC.RPN_MSB else MidiCC.RPN_LSB)] shr 25).toByte()
                arr = rpns
            }
            DteTarget.NRPN -> {
                dte_target_value = (controls[(if (isMsb) MidiCC.NRPN_MSB else MidiCC.NRPN_LSB)] shr 25).toByte()
                arr = nrpns
            }
        }
        val cur = arr[dte_target_value.toUnsigned()]
        if (isMsb)
            arr[dte_target_value.toUnsigned()] = (value shl 25).toUInt() + (cur and 0x1FE0000.toUInt())
        else
            arr[dte_target_value.toUnsigned()] = (cur and 0xFE000000.toUInt()) + (value shl 18).toUInt()
    }

    // This SHOULD NOT HAPPEN, but in case they were sent as legacy MIDI 1.0 messages...
    // increment as if it were sent in 7-bit precision -> translate it to 32-bit context
    fun processMidi1DteIncrement() {
        when (dteTarget) {
            DteTarget.RPN -> rpns[dte_target_value.toUnsigned()] += (1u shl 25)
            DteTarget.NRPN -> nrpns[dte_target_value.toUnsigned()] += (1u shl 25)
        }
    }

    // This SHOULD NOT HAPPEN, but in case they were sent as legacy MIDI 1.0 messages...
    // increment as if it were sent in 7-bit precision -> translate it to 32-bit context
    fun processMidi1DteDecrement() {
        when (dteTarget) {
            DteTarget.RPN -> rpns[dte_target_value.toUnsigned()] -= (1u shl 25)
            DteTarget.NRPN -> nrpns[dte_target_value.toUnsigned()] -= (1u shl 25)
        }
    }
}
