#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ******************************************************************************
# ZYNTHIAN PROJECT: Zynthian Control Device Driver
#
# Zynthian Control Device Driver for "Arturia BeatStep" (original, non-Pro)
#
# Confirmed ALSA device name via aconnect -i on Zynthian hardware:
#   client 28: 'Arturia BeatStep' [type=kernel,card=3]
#     0 'Arturia BeatStep MIDI 1'
#
# Install: /zynthian/zynthian-ui/zyngine/ctrldev/
# NOTE: class name must exactly match the filename (without .py)
#
# ******************************************************************************
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or any later version.
#
# ******************************************************************************
#
# Hardware:
#   16 velocity/pressure-sensitive pads (red/blue LEDs, limited SysEx control)
#   16 encoders (CC), 1 Level/Rate encoder (CC7), transport buttons
#
# LED feedback via SysEx (community reverse-engineered):
#   F0 00 20 6B 7F 42 02 00 10 [0x70+pad] [color] F7
#   Colors: 0x00=off  0x01=red  0x10=blue  0x11=magenta
#
# ******************************************************************************

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

# LED color constants
BS_LED_OFF     = 0x00
BS_LED_RED     = 0x01
BS_LED_BLUE    = 0x10
BS_LED_MAGENTA = 0x11

# Factory CNTRL-mode pad-to-note map (default preset)
# Index = zynpad slot (0-15), value = MIDI note BeatStep sends/receives
PAD_NOTES = [44, 45, 46, 47, 48, 49, 50, 51, 36, 37, 38, 39, 40, 41, 42, 43]
NOTE_TO_PAD = {note: pad for pad, note in enumerate(PAD_NOTES)}

# Encoder factory CC map (encoders 1-16)
ENC_CC   = [10, 74, 71, 76, 77, 93, 73, 75, 114, 18, 19, 16, 17, 91, 79, 72]
LEVEL_CC = 7

# Transport CCs (when BeatStep transport buttons set to CC mode, not MMC)
CC_STOP   = 118
CC_PLAY   = 119
CC_RECORD = 117


class zynthian_ctrldev_arturia_beatstep(zynthian_ctrldev_zynpad):

    dev_ids    = ["Arturia_BeatStep_MIDI_1"]
    dev_zynpad = True

    def init(self):
        self.light_off()

    def end(self):
        self.light_off()

    # ── LED helpers ──────────────────────────────────────────────────────────

    def _send_sysex(self, data_bytes):
        if self.idev_out is None:
            return
        try:
            msg = bytes(data_bytes)
            lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg))
        except Exception as e:
            logging.warning(f"BeatStep SysEx error: {e}")

    def _set_pad_led(self, pad_idx, color):
        """Set LED color for pad 0-15 via SysEx."""
        pad_id = 0x70 + pad_idx
        self._send_sysex([0xF0, 0x00, 0x20, 0x6B, 0x7F, 0x42,
                          0x02, 0x00, 0x10, pad_id, color, 0xF7])

    def light_off(self):
        for pad_idx in range(16):
            self._set_pad_led(pad_idx, BS_LED_OFF)

    # ── ZynPad LED refresh ───────────────────────────────────────────────────

    def _refresh_pad_led(self, pad):
        if self.idev_out is None:
            return
        try:
            state = self.zynseq.libseq.getSequenceState(self.zynseq.bank, pad)
            mode  = self.zynseq.libseq.getSequencePlayMode(self.zynseq.bank, pad)
        except Exception:
            self._set_pad_led(pad, BS_LED_OFF)
            return

        if mode == 0:
            color = BS_LED_OFF
        elif state == zynseq.SEQ_STOPPED:
            color = BS_LED_OFF
        elif state == zynseq.SEQ_PLAYING:
            color = BS_LED_BLUE
        elif state == zynseq.SEQ_STARTING:
            color = BS_LED_MAGENTA
        elif state == zynseq.SEQ_STOPPING:
            color = BS_LED_RED
        else:
            color = BS_LED_OFF

        self._set_pad_led(pad, color)

    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_pads(force=True)

    def refresh_pads(self, force=False):
        try:
            num_pads = self.zynseq.col_in_bank ** 2
        except Exception:
            num_pads = 16
        for pad in range(min(num_pads, 16)):
            self._refresh_pad_led(pad)

    def refresh_zynpad_bank(self):
        self.refresh_pads(force=True)

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

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

        # Note On → pad toggle
        if evtype == 0x9 and data2 > 0:
            if data1 in NOTE_TO_PAD:
                pad = NOTE_TO_PAD[data1]
                self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad)
                return True

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

        # CC events
        elif evtype == 0xB:
            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
            # Encoders and Level pass through for MIDI-learn
            if data1 in ENC_CC or data1 == LEVEL_CC:
                return False

        return False
