#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ******************************************************************************
# ZYNTHIAN PROJECT: Zynthian Control Device Driver
#
# Zynthian Control Device Driver for "Novation Launchpad X"
#
# Copyright (C) 2015-2025 Fernando Moyano <jofemodo@zynthian.org>
#                         Brian Walton <brian@riban.co.uk>
#                         Wapata <wapata.31@gmail.com>
#
#
# ******************************************************************************
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# For a full copy of the GNU General Public License see the LICENSE.txt file.
#
# ******************************************************************************

# Added CUIAs (some of them with autorepeat , thanks to @riban)
 


import logging
import threading
import time
from time import sleep

REPEAT_TIME = 0.3

# Zynthian specific modules
from zynlibs.zynseq import zynseq
from zyncoder.zyncore import lib_zyncore
from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad

# ------------------------------------------------------------------------------------------------------------------
# Novation Launchpad Pro MK3
# ------------------------------------------------------------------------------------------------------------------


class zynthian_ctrldev_launchpad_pro_mk3(zynthian_ctrldev_zynpad):

    dev_ids = ["Launchpad Pro MK3 IN 3"]

#    PAD_COLOURS = [6, 29, 17, 49, 66, 41, 23, 13, 
#    96, 2, 81, 82, 83, 84, 85, 86, 87]

    PAD_COLOURS = [6, 29, 17, 49, 66, 41, 23, 13]

    STARTING_COLOUR = 21
    STOPPING_COLOUR = 5
    SELECTED_BANK_COLOUR = 29
    STOP_ALL_COLOUR = 5
        
    def send_sysex(self, data):
        if self.idev_out is not None:
            msg = bytes.fromhex(f"F0 00 20 29 02 0E {data} F7")
            lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg))
            sleep(0.05)

    def get_note_xy(self, note):
        row = 8 - (note // 10)
        col = (note % 10) - 1
        return col, row


    def init(self):
        # Enter DAW session mode
        self.send_sysex("10 01")
        # Select session layout (session = 0x00, faders = 0x0D)
        self.send_sysex("00 00 00 00")
        ###################################################
        self._stop_auto_repeat_event = threading.Event()
        self._auto_repeat_thread = None
        ###################################################

        super().init()

    def end(self):
        super().end()
        # Exit DAW session mode
        self.send_sysex("10 00")
        # Select Keys layout (drums = 0x04, keys = 0x05, user = 0x06, prog = 0x7F)
        self.send_sysex("00 05 00 00")

    def update_seq_bank(self):
        if self.idev_out is None:
            return
        # logging.debug("Updating Launchpad Pro MK3 bank leds")
        for row in range(0, 7):
            note = 89 - 10 * row
            if row == self.zynseq.bank - 1:
                lib_zyncore.dev_send_ccontrol_change(
                    self.idev_out, 0, note, self.SELECTED_BANK_COLOUR)
            else:
                lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, note, 0)
        # Stop All button => Solid Red
        lib_zyncore.dev_send_ccontrol_change(
            self.idev_out, 0, 19, self.STOP_ALL_COLOUR)

        # Light-On Arrow Keys     
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 70, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 80, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 91, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 92, 10)

        # Light-On Lower Thin Buttons Bar     
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 1, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 2, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 3, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 4, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 5, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 6, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 7, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 8, 10)

        # Light-On Upper Thin Buttons Bar    
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 101, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 102, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 103, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 104, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 105, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 106, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 107, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 108, 10)
 
        # Light-On Left Buttons Column 
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 10, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 20, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 30, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 40, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 50, 10)
        lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 60, 10)
        


    def update_seq_state(self, bank, seq, state, mode, group):
        if self.idev_out is None or bank != self.zynseq.bank:
            return
        # logging.debug(f"Updating Launchpad Pro MK3 bank {bank} pad {seq} => state {state}, mode {mode}")
        col, row = self.zynseq.get_xy_from_pad(seq)
        note = 10 * (8 - row) + col + 1
        try:
            if mode == 0:
                chan = 0
                vel = 0
            elif state == zynseq.SEQ_STOPPED:
                chan = 0
                vel = self.PAD_COLOURS[group]
            elif state == zynseq.SEQ_PLAYING:
                chan = 2
                vel = self.PAD_COLOURS[group]
            elif state == zynseq.SEQ_STOPPING:
                chan = 1
                vel = self.STOPPING_COLOUR
            elif state == zynseq.SEQ_STARTING:
                chan = 1
                vel = self.STARTING_COLOUR
            else:
                chan = 0
                vel = 0
        except:
            chan = 0
            vel = 0
        # logging.debug("Lighting PAD {}, group {} => {}, {}, {}".format(seq, group, chan, note, vel))
        lib_zyncore.dev_send_note_on(self.idev_out, chan, note, vel)

    # Light-Off the pad specified with column & row
    def pad_off(self, col, row):
        note = 10 * (8 - row) + col + 1
        lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0)
    
    def midi_event(self, ev):
        # logging.debug(f"Launchpad Pro MK3 MIDI handler => {ev}")
        evtype = (ev[0] >> 4) & 0x0F
        # Note ON => launch/stop sequence
        if evtype == 0x9:
            note = ev[1] & 0x7F
            vel = ev[2] & 0x7F
            if vel > 0:
                col, row = self.get_note_xy(note)
                pad = self.zynseq.get_pad_from_xy(col, row)
                if pad >= 0:
                    self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad)
            return True
        # CC => arrows, scene change, stop all, and various CUIAs on other buttons
        elif evtype == 0xB:
            ccnum = ev[1] & 0x7F
            ccval = ev[2] & 0x7F

##################################################################################
# 2 ARROWS on the LEFT SIDE (up and down) and 2 ARROWS on the TOP (left and right)
# with autorepeatt
##################################################################################
############ Autorepeat ARROW_DOWN
            if ccnum == 70:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> arrow down released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_down, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> arrow down pressed <=")
############ Autorepeat ARROW_UP
            if ccnum == 80:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> arrow up released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_up, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> arrow up pressed <=")
############ Autorepeat ARROW_LEFT
            if ccnum == 91:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> arrow left released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_left, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> arrow left pressed <=")
############ Autorepeat ARROW_RIGHT
            if ccnum == 92:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> arrow right released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_right, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> arrow right pressed <=")

##############################
#16 thin BUTTONS at the BOTTOM
###############################################################
# 8 of them on the RIGHT emulate Zynpots 0 to 3 with autorepeat 
# the 4 up for (value +1) and the 4 down for (value -1)
###############################################################
#### Autorepeat ZYNPOT0_up
            if ccnum == 105:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot0_up  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot0_up, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot0_up pressed <=")
#### Autorepeat ZYNPOT1_up
            if ccnum == 106:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot1_up  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot1_up, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot1_up pressed <=")
#### Autorepeat ZYNPOT2_up
            if ccnum == 107:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot2_up  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot2_up, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot2_up pressed <=")
#### Autorepeat ZYNPOT3_up
            if ccnum == 108:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot3_up  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot3_up, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot3_up pressed <=")
#### Autorepeat ZYNPOT0_down
            if ccnum == 5:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot0_down  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot0_down, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot0_down pressed <=")
#### Autorepeat ZYNPOT1_down
            if ccnum == 6:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot1_down  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot1_down, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot1_down pressed <=")
#### Autorepeat ZYNPOT2_down
            if ccnum == 7:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot2_down  released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot2_down, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot2_down pressed <=")
#### Autorepeat ZYNPOT3_down
            if ccnum == 8:
                if ccval == 0:
                    # Release
                    if self._auto_repeat_thread:
                        self._stop_auto_repeat_event.set()
                        self._auto_repeat_thread.join()
                        self._auto_repeat_thread = None
                        logging.debug("=> zynpot3_down released <=") 
                else:
                    # Press
                    if not self._auto_repeat_thread:
                        self._stop_auto_repeat_event.clear()
                        self._auto_repeat_thread = threading.Thread(target=self._auto_repeat_zynpot3_down, daemon=True)
                        self._auto_repeat_thread.start()
                        logging.debug("=> zynpot3_down pressed <=")

#######################################################
# 8 of them on the left emulate zynswitches 
# the 4 up for short push , the 4 down for bold push
#######################################################
            if ccnum == 101 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0, 'S'])
            if ccnum == 102 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1, 'S'])
            if ccnum == 103 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2, 'S'])
            if ccnum == 104 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3, 'S'])
            if ccnum == 1 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0, 'B'])
            if ccnum == 2 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1, 'B'])
            if ccnum == 3 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2, 'B'])
            if ccnum == 4 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3, 'B'])

###################################
# 6 SQUARE BUTTONS on the LEFT SIDE
###################################
            if ccnum == 60 and ccval == 127:
                self.state_manager.send_cuia("SCREEN_ADMIN")
            if ccnum == 50 and ccval == 127:
                self.state_manager.send_cuia("SCREEN_MAIN_MENU")
            if ccnum == 40 and ccval == 127:
                self.state_manager.send_cuia("MENU")
            if ccnum == 30 and ccval == 127:
                self.state_manager.send_cuia("PRESET")
            if ccnum == 20 and ccval == 127:
                self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3, 'S']) # SELECT/YES
            if ccnum == 10 and ccval == 127:
                self.state_manager.send_cuia("BACK") # BACK/NO
#######################################################################################

            else:
                col, row = self.get_note_xy(ccnum)
                if col == 8:
                    if row < 7:
                        self.zynseq.select_bank(row + 1)
                    elif row == 7:
                            self.zynseq.libseq.stop()
            return True




        # SysEx
        elif ev[0] == 0xF0:
            logging.info(f"Received SysEx => {ev.hex(' ')}")
            return True

######################
#### ARROWS autorepeat 
#################################################################
    def _auto_repeat_down(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ARROW_DOWN")
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_up(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ARROW_UP")
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_left(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ARROW_LEFT")
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_right(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ARROW_RIGHT")
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################

#######################
#### ZYNPOTS autorepeat
#################################################################
    def _auto_repeat_zynpot0_up(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[0,1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot0_down(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[0,-1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot1_up(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[1,1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot1_down(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[1,-1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot2_up(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[2,1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot2_down(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[2,-1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot3_up(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[3,1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))
#################################################################
    def _auto_repeat_zynpot3_down(self):
        next_time = time.perf_counter()
        while not self._stop_auto_repeat_event.is_set():
            self.state_manager.send_cuia("ZYNPOT",[3,-1])
            next_time += REPEAT_TIME
            time.sleep(max(0, next_time - time.perf_counter()))

# ------------------------------------------------------------------------------