#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ******************************************************************************
# ZYNTHIAN PROJECT: Zynthian Control Device Driver
#
# Zynthian Control Device Driver for "M-Audio Oxygen Pro Mini" (32-key)
#
# Confirmed ALSA ports via aconnect -i on Zynthian hardware:
#   client 16: 'Oxygen Pro Mini' [type=kernel,card=0]
#     0 'Oxygen Pro Mini USB MIDI'   ← MIDI in (keys, pads, knobs, faders, buttons)
#     1 'Oxygen Pro Mini MIDI DIN'   ← physical 5-pin thru (unused here)
#     2 'Oxygen Pro Mini Mackie/HUI' ← LED feedback input per official M-Audio spec
#     3 'Oxygen Pro Mini Editor'     ← sysex preset editor only
#
# Official M-Audio spec (User Guide appendix):
#   "DAW LED control, and Mackie/HUI Heartbeat Messages → MIDIIN3 (Mackie/HUI)"
#   This means LED feedback IS possible by sending to port 2.
#
# Install: /zynthian/zynthian-ui/zyngine/ctrldev/
# NOTE: class name must exactly match the filename (without .py)
#
# ******************************************************************************
#
# FULL CONTROL MAP (Preset mode factory defaults unless noted):
#
#   FADERS 1-4:     CC 73, 74, 75, 76  → Zynthian mixer chains 0-3
#   KNOBS 1-4:      CC 20, 21, 22, 23  → MIDI-learn pass-through
#   PADS Bank A:    Notes 36,38,40,41,43,45,47,48 → zynpad slots 0-7
#   PADS Bank B:    Notes 37,39,42,44,46,49,51,54 → zynpad slots 8-15
#
#   TRANSPORT (User DAW / CC mode — configure in Oxygen Pro Editor):
#     Play=CC115, Stop=CC116, Record=CC117, Loop=CC118, <<= CC119, >>=CC120
#
#   BANK > / BANK <:  CC 121 / CC 122  → ZynSeq bank navigation
#     (configure in User DAW preset via Oxygen Pro Editor)
#
#   OCTAVE +/-:  CC 107 / CC 106  → ZynSeq bank up/down (fallback nav)
#
#   PAD BANK: hardware-internal — configure to send a CC in User DAW preset
#             then add that CC to _toggle_pad_bank() call in midi_event below
#
#   ARP / LATCH / CHORD / SCALE (function buttons):
#     hardware-internal in primary mode; send DAW channel messages in MODE mode
#     not intercepted by this driver in primary mode
#
#   NOTE: For transport and BANK CCs to arrive on port 0, the device must be
#   in Preset mode with a User DAW preset, OR in DAW mode with a User DAW that
#   maps transport to CC. Factory DAW mode sends Mackie messages on port 2.
#
# ******************************************************************************

import logging
from zyncoder.zyncore import lib_zyncore
from zynlibs.zynseq import zynseq
from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad

# ─────────────────────────────────────────────────────────────────────────────
# Pad note maps (factory Preset mode defaults)
# ─────────────────────────────────────────────────────────────────────────────
PAD_NOTES_BANK_A = [36, 38, 40, 41, 43, 45, 47, 48]   # zynpad slots 0-7
PAD_NOTES_BANK_B = [37, 39, 42, 44, 46, 49, 51, 54]   # zynpad slots 8-15

# note → (zynpad_slot, pad_bank_index 0/1)
NOTE_TO_PAD = {}
for _i, _n in enumerate(PAD_NOTES_BANK_A):
    NOTE_TO_PAD[_n] = (_i, 0)
for _i, _n in enumerate(PAD_NOTES_BANK_B):
    NOTE_TO_PAD[_n] = (_i + 8, 1)

# zynpad_slot → note (for LED feedback)
SLOT_TO_NOTE = {}
for _i, _n in enumerate(PAD_NOTES_BANK_A):
    SLOT_TO_NOTE[_i] = _n
for _i, _n in enumerate(PAD_NOTES_BANK_B):
    SLOT_TO_NOTE[_i + 8] = _n

# ─────────────────────────────────────────────────────────────────────────────
# Faders
# ─────────────────────────────────────────────────────────────────────────────
FADER_CC    = [73, 74, 75, 76]
CC_TO_FADER = {cc: i for i, cc in enumerate(FADER_CC)}

# ─────────────────────────────────────────────────────────────────────────────
# Knobs
# ─────────────────────────────────────────────────────────────────────────────
KNOB_CC = [20, 21, 22, 23]

# ─────────────────────────────────────────────────────────────────────────────
# Transport (User DAW CC mode — configure in Oxygen Pro Editor)
# ─────────────────────────────────────────────────────────────────────────────
CC_PLAY   = 115
CC_STOP   = 116
CC_RECORD = 117
CC_LOOP   = 118
CC_REW    = 119
CC_FF     = 120

# ─────────────────────────────────────────────────────────────────────────────
# Bank navigation (configure in User DAW preset — these are suggested defaults)
# ─────────────────────────────────────────────────────────────────────────────
CC_BANK_UP   = 121
CC_BANK_DOWN = 122

# ─────────────────────────────────────────────────────────────────────────────
# Octave buttons — factory default CCs (fallback for bank navigation)
# ─────────────────────────────────────────────────────────────────────────────
CC_OCTAVE_UP   = 107
CC_OCTAVE_DOWN = 106


class zynthian_ctrldev_maudio_oxygen_pro_mini(zynthian_ctrldev_zynpad):

    dev_ids      = ["Oxygen_Pro_Mini_USB_MIDI"]
    dev_zynpad   = True
    dev_zynmixer = True

    def init(self):
        self._current_pad_bank = 0  # 0=Bank A (slots 0-7), 1=Bank B (slots 8-15)
        logging.info("Oxygen Pro Mini: driver loaded")
        self._refresh_all_pad_leds()

    def end(self):
        self._all_leds_off()

    # ─────────────────────────────────────────────────────────────────────────
    # LED feedback
    # Sent via idev_out (port 0 USB MIDI back to device, or port 2 if manager
    # resolves output to Mackie/HUI port).
    # Note On with vel>0 = light on, vel=0 = light off.
    # ─────────────────────────────────────────────────────────────────────────

    def _send_pad_led(self, note, on):
        if self.idev_out is None:
            return
        try:
            lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0x7F if on else 0x00)
        except Exception as e:
            logging.warning(f"Oxygen Pro Mini LED note {note}: {e}")

    def _refresh_pad_led(self, slot):
        if slot not in SLOT_TO_NOTE:
            return
        note = SLOT_TO_NOTE[slot]
        try:
            state = self.zynseq.libseq.getSequenceState(self.zynseq.bank, slot)
            mode  = self.zynseq.libseq.getSequencePlayMode(self.zynseq.bank, slot)
        except Exception:
            self._send_pad_led(note, False)
            return

        if mode == 0:
            lit = False   # empty
        elif state in (zynseq.SEQ_PLAYING, zynseq.SEQ_STARTING, zynseq.SEQ_STOPPING):
            lit = True
        else:
            lit = False   # stopped/has-content but not running

        self._send_pad_led(note, lit)

    def _refresh_all_pad_leds(self):
        for slot in range(16):
            self._refresh_pad_led(slot)

    def _all_leds_off(self):
        for note in PAD_NOTES_BANK_A + PAD_NOTES_BANK_B:
            self._send_pad_led(note, False)

    def _toggle_pad_bank(self):
        """Switch active pad bank between A (0-7) and B (8-15)."""
        self._current_pad_bank = 1 - self._current_pad_bank
        bank_name = 'A' if self._current_pad_bank == 0 else 'B'
        logging.info(f"Oxygen Pro Mini: pad bank → {bank_name}")
        self._refresh_all_pad_leds()

    # ─────────────────────────────────────────────────────────────────────────
    # ZynPad interface
    # ─────────────────────────────────────────────────────────────────────────

    def light_off(self):
        self._all_leds_off()

    def refresh_pads(self, force=False):
        self._refresh_all_pad_leds()

    def update_seq_state(self, bank, seq, state, mode, group):
        if bank != self.zynseq.bank:
            return
        self._refresh_pad_led(seq)

    def update_seq_bank(self):
        self._refresh_all_pad_leds()

    def refresh_zynpad_bank(self):
        self._refresh_all_pad_leds()

    def refresh(self, force=False):
        pass  # no motorized faders — no outgoing feedback on fader position

    # ─────────────────────────────────────────────────────────────────────────
    # MIDI event handler
    # ─────────────────────────────────────────────────────────────────────────

    def midi_event(self, ev):
        """
        ev = bytes/bytearray: ev[0]=status, ev[1]=data1, ev[2]=data2
        Returns True = consumed, False = pass through to MIDI routing.
        """
        evtype = (ev[0] >> 4) & 0x0F
        data1  = ev[1] & 0x7F
        data2  = (ev[2] & 0x7F) if len(ev) > 2 else 0

        # ── Note On ──────────────────────────────────────────────────────────
        if evtype == 0x9 and data2 > 0:
            if data1 in NOTE_TO_PAD:
                slot, bank = NOTE_TO_PAD[data1]
                if bank == self._current_pad_bank:
                    self.zynseq.libseq.togglePlayState(self.zynseq.bank, slot)
                    self._refresh_pad_led(slot)
                return True
            # Keys: not consumed, fall through to normal MIDI routing

        # ── Note Off: consume pad releases (toggle-based, ignore release) ────
        elif evtype == 0x8 or (evtype == 0x9 and data2 == 0):
            if data1 in NOTE_TO_PAD:
                return True

        # ── CC ───────────────────────────────────────────────────────────────
        elif evtype == 0xB:

            # Faders → mixer chain volume
            if data1 in CC_TO_FADER:
                chain_idx  = CC_TO_FADER[data1]
                normalized = data2 / 127.0
                try:
                    chains = list(self.zyngui.chain_manager.get_active_chains().keys())
                    if chain_idx < len(chains):
                        self.zyngui.zynmixer.set_level(chains[chain_idx], normalized)
                except Exception as e:
                    logging.warning(f"Oxygen Pro Mini fader {chain_idx}: {e}")
                return True

            # Transport
            if data1 == CC_STOP and data2 > 0:
                self.state_manager.send_cuia("STOP")
                return True
            if data1 == CC_PLAY and data2 > 0:
                self.state_manager.send_cuia("TOGGLE_PLAY")
                return True
            if data1 == CC_RECORD and data2 > 0:
                self.state_manager.send_cuia("TOGGLE_RECORD")
                return True
            if data1 == CC_LOOP and data2 > 0:
                self.state_manager.send_cuia("TOGGLE_LOOP")
                return True
            if data1 == CC_REW and data2 > 0:
                self.state_manager.send_cuia("ARROW_LEFT")
                return True
            if data1 == CC_FF and data2 > 0:
                self.state_manager.send_cuia("ARROW_RIGHT")
                return True

            # BANK > / BANK < → ZynSeq bank navigation
            if data1 == CC_BANK_UP and data2 > 0:
                try:
                    self.zynseq.select_bank(min(self.zynseq.bank + 1, 64))
                except Exception as e:
                    logging.warning(f"Oxygen Pro Mini bank up: {e}")
                return True
            if data1 == CC_BANK_DOWN and data2 > 0:
                try:
                    self.zynseq.select_bank(max(self.zynseq.bank - 1, 1))
                except Exception as e:
                    logging.warning(f"Oxygen Pro Mini bank down: {e}")
                return True

            # Octave buttons as fallback bank navigation
            if data1 == CC_OCTAVE_UP and data2 > 0:
                try:
                    self.zynseq.select_bank(min(self.zynseq.bank + 1, 64))
                except Exception:
                    pass
                return True
            if data1 == CC_OCTAVE_DOWN and data2 > 0:
                try:
                    self.zynseq.select_bank(max(self.zynseq.bank - 1, 1))
                except Exception:
                    pass
                return True

            # Knobs: pass through for MIDI-learn
            if data1 in KNOB_CC:
                return False

            # ── PAD BANK toggle ──────────────────────────────────────────────
            # PAD BANK is hardware-internal by default. To enable it here,
            # configure PAD BANK button to send a CC in your User DAW preset
            # (e.g. CC 123), then uncomment and set the CC number below:
            #
            # PAD_BANK_CC = 123
            # if data1 == PAD_BANK_CC and data2 > 0:
            #     self._toggle_pad_bank()
            #     return True

        return False
