import jack, jack_server
import numpy as np
import os, sys, io, re
from gpiozero import LED, PWMLED, Button, LEDBoard, LEDCharDisplay, LEDMultiCharDisplay, LEDCharFont
from time import sleep, time
from pydub import AudioSegment
from datetime import datetime
sys.path.append('./pyfluidsynth')
import fluidsynth
from threading import Thread, Event
import init
import subprocess

# ------- Variables and Flags Initialization ------

LENGTH = 0  # Length of the first recording or Master Track (0). All subsequent recordings will be quantized to a multiple of this.
number_of_tracks = 16  # Number of Tracks
selected_loop = 0  # Pointer to the selected Loop/Track
length_of_sequence = 0  # Length of the Sequenced Tracks
seq_counter = 0  # Counter for Sequenced Tracks 
seq_step = 0
setup_is_recording = False  # Set to True when track 1 recording button is first pressed
setup_donerecording = False  # Set to true when first track 1 recording is done
Mode = 3  # Pointer to the Mode for UI
Preset = 0  # Pointer to the Selected Preset
Bank = 0  # Pointer to the Selected Bank
Session = 0  # Pointer to the Selected Session to Import
sfid = 0  # SoundFont id of the Loaded Bank
init_volume = 14  # Initial volume for each Track
max_volume = 20  # Max volume for each Track
display_data = ""  # Secondary info to show on Display
display_count = 0  # Timer to show secondary info on Display
synth_initialized = False  # Flag
set_recording_file = False  # Flag to set Recording Audio Session waiting to starting of Master Track
rec_file = False  # Flag for the Recording Audio Session activity
recordings_dir = "./recordings/"  # Dir where Recording of Audio Sessions will be stored
max_amplitude = 32767
volume_up = True  # Flag to Increase/Decrease Volume of Tracks
rec_was_held = False
mute_was_held = False
clear_was_held = False
prev_was_held = False
next_was_held = False
mode_was_held = False

# ------- END of Variables and Flags ------

# ----------- USER INTERFACE --------------
# Behavior when MODEBUTTON is pressed
def Change_Mode():
    global Mode, mode_was_held, audio_buffer
    if not mode_was_held:
        Mode = (Mode + 1) % 3  # Change to the next Mode
        print('----------= Changed to Mode=', str(Mode), '\n')
    mode_was_held = False

# Behavior when MODEBUTTON is held
def restart_program():
    print("---------- Restarting LooPyStation ----------")
    TurningOff()
    Closing_Jack_Client()
    python = sys.executable  # Gets the actual python interpreter
    os.execv(python, [python] + sys.argv)

# Behavior when PREVBUTTON is pressed
def Prev_Button_Press():
    global selected_loop, Preset, prev_was_held, Session, display_data
    if not prev_was_held:
        if Mode == 0 and setup_donerecording:
            selected_loop = (selected_loop - 1) % number_of_tracks
            print('-= Prev Loop =---> ', selected_loop, '\n')
            debug()
        elif Mode == 1:
            if len(sf2_list) > 0 and synth_initialized:
                if Preset >= 1:
                    Preset -= 1
                    ChangePreset()
        elif Mode == 2:
            if Session >= 1:
                Session -= 1
                print("Selected Session = ", str(Session), " - ", str(sessions[Session]))
                display_data = str(Session).zfill(2)
    prev_was_held = False
    change_volume_event.clear()  # Detener la disminución acelerada

# Behavior when PREVBUTTON is held
def Prev_Button_Held():
    global Bank, prev_was_held, volume_up
    if Mode == 0 and setup_donerecording and loops[selected_loop].initialized >= 1:
        volume_up = False
        change_volume_event.set()  # Iniciar la disminución acelerada
    elif Mode == 1:
        if len(sf2_list) > 0 and synth_initialized:
            if Bank >= 1:
                Bank -= 1
                ChangeBank()
    prev_was_held = True

# Behavior when NEXTBUTTON is pressed
def Next_Button_Press():
    global selected_loop, Preset, next_was_held, Session, display_data
    if not next_was_held:
        if Mode == 0 and setup_donerecording:
            selected_loop = (selected_loop + 1) % number_of_tracks
            print('-= Next Loop =---> ', selected_loop, '\n')
            debug()
        if Mode == 1:
            if len(sf2_list) > 0 and synth_initialized:
                if Preset < 125:
                    Preset += 1
                    ChangePreset()
        elif Mode == 2:
            if Session < len(sessions) - 1:
                Session += 1
                print("Selected Session = ", str(Session), " - ", str(sessions[Session]))
                display_data = str(Session).zfill(2)
    next_was_held = False
    change_volume_event.clear()

# Behavior when NEXTBUTTON is held
def Next_Button_Held():
    global Bank, next_was_held, volume_up
    if Mode == 0 and setup_donerecording and loops[selected_loop].initialized >= 1:
        volume_up = True
        change_volume_event.set()
    elif Mode == 1:
        if len(sf2_list) > 0 and synth_initialized:
            if Bank < len(sf2_list) - 1:
                Bank += 1
                ChangeBank()
    next_was_held = True

# Behavior when RECBUTTON is pressed
def Rec_Button_Pressed():
    if Mode == 0 or Mode == 1:
        loops[selected_loop].set_recording()
    elif Mode == 2:
        rec_audio_session()

# Behavior when UNDOBUTTON is pressed
def Undo_Button_Pressed():
    if Mode == 0 or Mode == 1:
        global clear_was_held
        if not clear_was_held:
            loops[selected_loop].undo()
        clear_was_held = False

# Behavior when UNDOBUTTON is held
def Undo_Button_Held():
    if Mode == 0 or Mode == 1:
        global clear_was_held
        clear_was_held = True
        loops[selected_loop].clear()
    elif Mode == 2:
        import_session()

# Behavior when MUTEBUTTON is pressed
def Mute_Button_Pressed():
    if Mode == 0 or Mode == 1:
        global mute_was_held
        if setup_donerecording:
            if not mute_was_held:
                loops[selected_loop].toggle_mute()
            mute_was_held = False

# Behavior when MUTEBUTTON is held
def Mute_Button_Held():
    if Mode == 0 or Mode == 1:
        global mute_was_held
        if setup_donerecording:
            mute_was_held = True
            loops[selected_loop].toggle_solo()
    elif Mode == 2:
        if setup_donerecording:
            export_session()
        else:
            print("Nothing to Export")

#------------------------------------------------------------------------------------------------

def esperar_a_jack():
    print("Buscando el servidor JACK 'default'...")
    
    while True:
        try:
            # Intentamos crear un cliente temporal para verificar el servidor
            # Si el servidor no existe o no responde, esto lanza una excepción
            client = jack.Client("Verificador")
            
            # Si llegamos aquí, es que ha conectado con éxito
            print("¡JACK detectado y funcionando!")
            client.close() # Cerramos el cliente temporal
            break          # Salimos del bucle
            
        except jack.JackOpenError:
            print("JACK no disponible. Reintentando en 1 segundo...", end='\r')
            sleep(1)
        except Exception as e:
            print(f"Error inesperado: {e}")
            sleep(1)

# Turns Off the Looper and exits
def TurningOff():
    global synth_initialized
    if synth_initialized:
        print("--- Deteniendo FluidSynth ---")
        try:
            fs.delete() 
            print("FluidSynth borrado correctamente.")
        except Exception as e:
            print(f"Error al borrar FluidSynth: {e}")
        synth_initialized = False
        os.system("pkill -9 fluidsynth 2>/dev/null")
        sleep(1)
    print("Deactivating JACK Client")
    client.deactivate()
    PowerOffLeds()
    Closing_Jack_Client()
    print('Done...')

def Closing_Jack_Client():
    print("---------- Cerrando cliente JACK...")
    print("Audio liberado. ¡Adiós!")

def rec_audio_session():
    global audio_buffer, set_recording_file
    if not rec_file:  # If Flag to record on disk is False
        audio_buffer = io.BytesIO()  # Creates Audio Buffer to be recorded on Disk
        set_recording_file = True  # Flag to Start Recording on disk by the loop_callback
        print("---= Recording to file =---")
    else:
        audio_buffer.seek(0)
        audio_segment = AudioSegment.from_raw(audio_buffer, sample_width=2, frame_rate=48000, channels=1)
        date_time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        output_file_name = f"{recordings_dir}LooPyStation_output_{date_time_now}.mp3"
        audio_segment.export(output_file_name, format="mp3", bitrate="320k")  # Write file to disk
        print("---= MP3 File saved like: ", output_file_name)
        set_recording_file = False  # Flag to Stop Recording on disk by the loop_callback

def export_session():  # In Mode 2, holding Mute Button, exports all the initialized tracks to wav
    date_time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    print(f"-----= Exporting Session {date_time_now}")

    for i in range(number_of_tracks):
        if loops[i].initialized >= 1:
            audio_buffer = loops[i].dub_audio[:loops[i].length].tobytes()
            write_track_file_session(audio_buffer, date_time_now, i, 1)
            if loops[i].initialized >= 2:
                audio_buffer = loops[i].main_audio[:loops[i].length].tobytes()
                write_track_file_session(audio_buffer, date_time_now, i, 2)
    print("Session 'session_" + str(date_time_now) + "' SAVED Successfully")
    print("-----------------------")
    print("**** Sessions List ****")
    init.list_sessions(Session)

def write_track_file_session(audio_buffer, date_time_now, i, initia):
    audio_buffer = io.BytesIO(audio_buffer)
    audio_segment = AudioSegment.from_raw(audio_buffer, sample_width=2, frame_rate=48000, channels=1)
    output_file_name = init.sessions_dir + "session_" + str(date_time_now) + "-track_" + str(i).zfill(
        2) + "_" + str(initia).zfill(1) + "-" + str(loops[i].volume).zfill(2) + "-" + str(loops[i].undo_mode).zfill(1) + ".wav"
    audio_segment.export(output_file_name, format="wav")  # Write file to disk
    print("   * Session Track - file saved: ", output_file_name)

def import_session():
    Thread(target=import_session_worker, daemon=True).start()

def import_session_worker():  # In Mode 2, holding Undo Button, imports the selected (with Prev and Next Buttons) session from the ones recorded at ./recordings
    sessions_list = init.list_sessions(Session)
    selected_session = sessions_list[0]
    sessions = sessions_list[1]
    if len(sessions) > 0:
        global setup_donerecording, setup_is_recording, selected_loop
        for loop in loops:
            loop.__init__()  # Initialize ALL
        for file in selected_session:
            if len(file) >= 43:  # Make sure the file is at least 43 characters long
                session_track_number = int(file[34:36])  # Extract the chars 35 and 36 that are the Track Number
                session_track_init = int(file[37:38])  # Extract the chars 38 that is the Track Volume
                session_track_volume = int(file[39:41])  # Extract the chars 40 and 41 that are the Track Volume
                session_track_undo = int(file[42:43])  # Extract the chars 43 that is the Track Volume
                print(f"File: {file} ---> Track: {session_track_number}")
                session_file_path = init.sessions_dir + file
                load_wav(session_file_path, session_track_number, session_track_init, session_track_volume, session_track_undo)
            else:
                print(f"The file '{file}' has not enough chars in the name.")
        setup_donerecording = True
        setup_is_recording = False
        loops[0].is_waiting_rec = False
        selected_loop = 0
        print("---= Session Imported Succesfully :-D =---", '\n')
        debug()

def load_wav(session_file_path, session_track_number, session_track_init, session_track_volume, session_track_undo):
    global LENGTH
    try:
        audio_segment = AudioSegment.from_file(session_file_path, format="wav")  # Loads wav file
        audio_data = np.array(audio_segment.get_array_of_samples(), dtype=np.int16)  # Convert to NumPy
        num_blocks = len(audio_data) // CHUNK  # Length in CHUNKs
        # Copy the data to main_audio and restore initializations, lengths, length_factors and writep
        if session_track_number == 0:
            LENGTH = num_blocks
        if session_track_init ==1:
            loops[session_track_number].dub_audio[:num_blocks] = audio_data[:num_blocks * CHUNK].reshape(num_blocks, CHUNK)
        if session_track_init ==2:
            loops[session_track_number].main_audio[:num_blocks] = audio_data[:num_blocks * CHUNK].reshape(num_blocks, CHUNK)
        loops[session_track_number].length = num_blocks
        loops[session_track_number].volume = session_track_volume
        loops[session_track_number].initialized = session_track_init
        loops[session_track_number].undo_mode = session_track_undo
        loops[session_track_number].writep = num_blocks - 1
        loops[session_track_number].length_factor = loops[session_track_number].length / loops[0].length
        loops[session_track_number].is_playing = True
        print(f"Track: {session_track_number} - Archivo cargado: {session_file_path} | Longitud: {num_blocks} bloques.")
    except Exception as e:
        print(f"Error al cargar {session_file_path}: {e}")

# Converts pcm2float array
def pcm2float(sig, dtype='float32'):
    sig = np.asarray(sig)
    if sig.dtype.kind not in 'iu':
        raise TypeError("'sig' must be an array of integers")
    dtype = np.dtype(dtype)
    if dtype.kind != 'f':
        raise TypeError("'dtype' must be a floating point type")

    i = np.iinfo(sig.dtype)
    abs_max = 2 ** (i.bits - 1)
    offset = i.min + abs_max
    return (sig.astype(dtype) - offset) / abs_max

# Converts float2pcm array
def float2pcm(sig, dtype='int16'):
    sig = np.asarray(sig)
    if sig.dtype.kind != 'f':
        raise TypeError("'sig' must be a float array")
    dtype = np.dtype(dtype)
    if dtype.kind not in 'iu':
        raise TypeError("'dtype' must be an integer type")

    i = np.iinfo(dtype)
    abs_max = 2 ** (i.bits - 1)
    offset = i.min + abs_max
    return (sig * abs_max + offset).clip(i.min, i.max).astype(dtype)

# Turn-Off all the Leds
def PowerOffLeds():
    init.RECLEDR.value = 0
    init.RECLEDG.value = 0
    init.PLAYLEDR.value = 0
    init.PLAYLEDG.value = 0

# Event to control the volume change
change_volume_event = Event()

def change_volume_with_acceleration():
    global selected_loop, display_data
    base_interval = 0.5  # Base interval in seconds
    acceleration_factor = 0.7  # Acceleration Factor
    min_interval = 0.1  # Min. Interval between volume changes in seconds
    while True:  # Infinite Loop to keep the thread running
        if change_volume_event.is_set():  # Test if event is active
            interval = base_interval
            while change_volume_event.is_set():  # While Button is pressed
                if volume_up == False:
                    if loops[selected_loop].volume >= 1:
                        loops[selected_loop].volume -= 1
                        print('Volume Decreased=', loops[selected_loop].volume, '\n')
                else:
                    if loops[selected_loop].volume <= max_volume - 1:
                        loops[selected_loop].volume += 1
                        print('Volume Increased=', loops[selected_loop].volume, '\n')
                display_data = str(loops[selected_loop].volume).zfill(2)
                debug()
                # Reducir el intervalo para acelerar
                interval = max(min_interval, interval * acceleration_factor)
                sleep(interval)
        else:
            sleep(0.1)  # Avoids the high CPU load when the event is not active

# Start the thread to change volume with acceleration
volume_thread = Thread(target=change_volume_with_acceleration, daemon=True)
volume_thread.start()

# Changes the FluidSynth Preset
def ChangePreset():
    fs.program_select(0, sfid, 0, Preset)
    print('----- Bank: ', str(Bank), ' - ', str(sf2_list[Bank]),' / Preset: ',  ' - ', str(Preset), '\n')

# Changes the FluidSynth Bank
def ChangeBank():
    global display_data, sfid, Preset
    display_data = str(Bank).zfill(2)
    fs.sfunload(sfid)
    sfid = fs.sfload("./sf2/" + str(sf2_list[Bank]))
    fs.program_select(0, sfid, 0, 0)
    Preset = 0
    print('----- Bank: ', str(Bank), ' - ', str(sf2_list[Bank]),' / Preset: ',  ' - ', str(Preset), '\n')

# Assign all the Capture ports to Looper Input
def all_captures_to_input():
    print("---= all_captures_to_input() =---", '\n')
    capture = client.get_ports(is_audio=True, is_physical=True, is_output=True)
    print("Capture ports:", capture, "\n")
    print("input_port:", input_port, "\n")
    print("input_port name:", input_port.name, "\n")
    print("Is client active:", client.status, "\n")
    
    for src in capture:
        print(f"Intentando conectar {src.name} -> {input_port.name}")
        try:
            client.connect(src, input_port)
        except jack.JackErrorCode as e:
            print(f"Error {e}")

# Assign the Looper Output to all the Playback ports
def output_to_all_playbacks():
    print("---= output_to_all_playbacks() =---", '\n')
    playback = client.get_ports(is_audio=True, is_physical=True, is_input=True)
    print(playback, '\n')
    if not playback:
        raise RuntimeError("No physical playback ports")
    for dest in playback:
        client.connect(output_port, dest)

# Assign all the Capture ports to Looper Input
def connect_fluidsynth():
    client.connect('system:midi_capture_1', 'fluidsynth:midi_00')
    client.connect('fluidsynth:left', 'LooPyStation:input_1')
    client.connect('fluidsynth:right', 'LooPyStation:input_1')

def conectar_grabacion_paralela(nombre_cliente_jack="LooPyStation"):
    try:
        playback_1 = client.get_port_by_name("system:playback_1")
        playback_2 = client.get_port_by_name("system:playback_2")
        mi_entrada = client.get_port_by_name(f"{nombre_cliente_jack}:input_1")
        mi_salida = client.get_port_by_name(f"{nombre_cliente_jack}:output_1")

        # 1. Intentamos obtener fuentes de los altavoces
        fuentes_l = client.get_all_connections(playback_1)
        fuentes_r = client.get_all_connections(playback_2)

        # 2. Si están vacías (como en tu caso), buscamos mod-monitor manualmente
        if not fuentes_l:
            print("DEBUG: Altavoces vacíos. Buscando mod-monitor...")
            try:
                fuentes_l = [client.get_port_by_name("mod-monitor:out_1")]
                fuentes_r = [client.get_port_by_name("mod-monitor:out_2")]
            except jack.JackError:
                print("DEBUG: mod-monitor no encontrado. Buscando efectos genéricos...")
                # Plan C: Buscar cualquier puerto de salida de un efecto
                fuentes_l = client.get_ports(is_output=True, is_audio=True)
                fuentes_l = [p for p in fuentes_l if "effect_" in p.name and "out" in p.name.lower()]

        print("\n--- Estableciendo conexiones de grabación ---")

        # 3. Realizar conexiones a tu entrada
        for source in fuentes_l:
            if source.name != mi_salida.name:
                try:
                    client.connect(source, mi_entrada)
                    print(f"CONECTADO: {source.name} -> {mi_entrada.name}")
                except jack.JackError: pass

        for source in fuentes_r:
            if source.name != mi_salida.name:
                try:
                    client.connect(source, mi_entrada)
                    print(f"CONECTADO: {source.name} -> {mi_entrada.name}")
                except jack.JackError: pass

        # 4. Asegurar que escuchas algo (conectar mod-monitor a altavoces si estaba suelto)
        for source in fuentes_l:
            try: client.connect(source, playback_1)
            except: pass
        for source in fuentes_r:
            try: client.connect(source, playback_2)
            except: pass
            
        # Conectar tu salida a altavoces
        try:
            client.connect(mi_salida, playback_1)
            client.connect(mi_salida, playback_2)
            print(f"CONECTADO: {mi_salida.name} -> altavoces")
        except jack.JackError: pass
        
        print("----------------------------------------------\n")

    except jack.JackError as e:
        print(f"Error crítico en las conexiones: {e}")

# Debug prints info on stdout
def debug():
    print('   |init|rec|wait|play|waiP|waiM|Solo|IsUn|Vol |MaxP |WriP   |Leng')
    for i in range(number_of_tracks):
        print(str(i).zfill(2), '|',
              int(loops[i].initialized), ' |',
              int(loops[i].is_recording), '|',
              int(loops[i].is_waiting_rec), ' |',
              int(loops[i].is_playing), ' |',
              int(loops[i].is_waiting_play), ' |',
              int(loops[i].is_waiting_mute), ' |',
              int(loops[i].is_solo), ' |',
              int(loops[i].undo_mode), ' |',
              str(int(loops[i].volume)).zfill(2), '|',
              str(int(loops[i].maxpeak)).zfill(3), '|',
              str(int(loops[i].writep)).zfill(5), '|',
              int(loops[i].length))
    print('setup_donerecording=', setup_donerecording, ' setup_is_recording=', setup_is_recording, 'output_volume=', str(output_volume)[0:4])
    print('length=', loops[selected_loop].length, 'LENGTH=', LENGTH, 'length_factor=', loops[selected_loop].length_factor)
    print('|', ' '*9,'|',' '*9,'|', ' '*9,'|',' '*9,'|')

# Checks which loops are recording/playing/waiting and lights up LEDs and Display accordingly
def show_status():
    global display_data, display_count
    # If Prev / Next Buttons are Pressed, 8-seg. Display shows selected selected_loop / Preset (depends of Mode)
    if display_data == "":
        if Mode == 0:
            init.display.value = str(selected_loop).zfill(2)
        elif Mode == 1:
            init.display.value = (str(Preset).zfill(2)[-2], str(Preset).zfill(2)[-1] + '.')
        elif Mode == 2:
            init.display.value = "--"
    else:  # Else, if Prev / Next Buttons are Held, display shows Volume / Bank (depends of Mode)
        if display_count <= 4:
            init.display.value = display_data[-2:]
            display_count += 1
        else:
            display_count = 0
            display_data = ""

    # Leds Status for Rec Button ---------------------------
    if Mode == 0 or Mode == 1:
        if loops[selected_loop].is_recording:
            init.RECLEDR.value = 1
            init.RECLEDG.value = 0

        elif loops[selected_loop].is_waiting_rec:
            init.RECLEDR.value = 1
            init.RECLEDG.value = 0.3
        elif setup_donerecording or not loops[selected_loop].is_recording:
            init.RECLEDR.value = 0
            init.RECLEDG.value = 0
    elif Mode == 2:
        if rec_file:
            init.RECLEDR.value = 1
            init.RECLEDG.value = 0
        else:
            init.RECLEDR.value = 0
            init.RECLEDG.value = 0

    # Leds Status for Play Button ---------------------------
    if loops[selected_loop].is_waiting_play or loops[selected_loop].is_waiting_mute:
        init.PLAYLEDR.value = 1
        init.PLAYLEDG.value = 0.3
    elif loops[selected_loop].is_playing:
        init.PLAYLEDR.value = 0
        init.PLAYLEDG.value = 1
    else:
        init.PLAYLEDR.value = 0
        init.PLAYLEDG.value = 0

    # Leds Status for Undo Button ---------------------------
    init.UNDOLEDR.value = 0
    init.UNDOLEDG.value = 0

# ----------------------------------------------------------------------------------

# ---= Start =----------------------------------------------------------------------
print('\n', '----- Starting LooPyStation... -----', '\n')

sf2_list = init.list_sf2()  # Reads all the sf2 files
sessions_list = init.list_sessions(Session)  # Reads all the exported sessions
selected_session = sessions_list[0]
sessions = sessions_list[1]

# Defining functions of all the buttons during jam session...
init.PREVBUTTON.when_released = Prev_Button_Press
init.PREVBUTTON.when_held = Prev_Button_Held
init.NEXTBUTTON.when_released = Next_Button_Press
init.NEXTBUTTON.when_held = Next_Button_Held
init.MODEBUTTON.when_released = Change_Mode
init.MODEBUTTON.when_held = restart_program
init.RECBUTTON.when_pressed = Rec_Button_Pressed
init.UNDOBUTTON.when_released = Undo_Button_Pressed
init.UNDOBUTTON.when_held = Undo_Button_Held
init.PLAYBUTTON.when_released = Mute_Button_Pressed
init.PLAYBUTTON.when_held = Mute_Button_Held

print("----- Conectando a servidor JACK existente... -----")

# Shows "[]" on 8-seg Display
init.display.value = "Cc"

# Llamamos a la función
esperar_a_jack()

# Aquí continúa el resto de tu código
print("Continuando con la ejecución de LooPyStation...")
Mode = 0
for i in range(6):  # Animation on Display
    if i % 3 == 0:
        init.display.value = "++"
    elif i % 3 == 1:
        init.display.value = "--"
    else:
        init.display.value = "__"
    sleep(0.1)

# Initializing JACK Client
client = jack.Client("LooPyStation")
print('----- Jack Client LooPyStation Initialized -----','\n')
sleep(1)

# Leer parámetros directamente del servidor JACK
RATE = client.samplerate
CHUNK = client.blocksize

# Read settings from settings.prt
settings_file = open('./settings.prt', 'r')
parameters = settings_file.readlines()
settings_file.close()

latency_in_milliseconds = int(parameters[2])
LATENCY = round((latency_in_milliseconds/1000) * (RATE/CHUNK))  # Latency in buffers
overshoot_in_milliseconds = int(parameters[5])  # Allowance in milliseconds for pressing 'stop recording' late
OVERSHOOT = round((overshoot_in_milliseconds/1000) * (RATE/CHUNK))  # Allowance in buffers
MAXLENGTH = int(12582912 / CHUNK)  # 96mb of audio in total
JACK_CAPTURE_PORTS = int(parameters[6])

print(f"JACK Server - RATE: {RATE} / CHUNK: {CHUNK}")
print('LATENCY correction (buffers): ', str(LATENCY), '\n')

# Create and initialize buffers
play_buffer = np.zeros([CHUNK], dtype=np.int16)  # Buffer to hold mixed audio from all tracks
silence = np.zeros([CHUNK], dtype=np.int16)  # A buffer containing silence
current_rec_buffer = np.zeros([CHUNK], dtype=np.int16)  # Buffer to hold in_data
output_volume = np.float32(1.0)  # Mixed output (sum of audio from tracks) is multiplied by output_volume before being played. Not used by now
fade_in = np.linspace(0, 1, CHUNK)
fade_out = np.linspace(1, 0, CHUNK)

class audioloop:
    def __init__(self):
        self.initialized = 0
        self.length_factor = 1
        self.length = 0
        self.maxpeak = 0
        self.readp = 0
        self.writep = 0
        self.is_recording = False
        self.is_playing = False
        self.is_waiting_rec = False
        self.is_waiting_play = False
        self.is_waiting_mute = False
        self.undo_mode = 0
        self.is_solo = False
        self.is_sequence = False
        self.volume = init_volume
        self.preceding_buffer = np.zeros([CHUNK], dtype=np.int16)
        self.main_audio = np.zeros([MAXLENGTH, CHUNK], dtype=np.int16)  # self.main_audio contain main audio data in arrays of CHUNKs.
        self.dub_audio = np.zeros([MAXLENGTH, CHUNK], dtype=np.int16)

    # increment_pointers() increments pointers and, when restarting while recording
    def increment_pointers(self):
        if self.readp == self.length - 1:
            self.readp = 0
            print(' '*60, end='\r')
        else:
            self.readp += 1
        progress = (loops[0].readp / (loops[0].length + 1))*50
        self.writep = (self.writep + 1) % self.length
        print('#'*int(progress), end='\r')

    # read_buffer() reads and returns a buffer of audio from the loop
    def read_buffer(self):
        global rec_file, seq_counter, seq_step

        # Tasks to do each time the master loop restarts
        if setup_donerecording and loops[0].readp == 0:
            # Turns On 0,1s the Red Led of PLAYBUTTON to mark the starting of Master Loop
            init.UNDOLEDR.value = 1
            init.UNDOLEDG.value = 0
            # Trigger the rec_file flag
            if set_recording_file:
                rec_file = True
            else:
                rec_file = False
            #If Sequence is Activated
            if loops[12].is_sequence:
                if loops[12+seq_step].readp == 0:
                    seq_step = (seq_step + 1) % seq_counter
                for i in range(seq_counter):
                    if i == seq_step:
                        loops[12+i].is_playing = True
                    else:
                        loops[12+i].is_playing = False
            else:
                if loops[12+seq_step].readp == 0:
                    for i in range(seq_counter):
                        if i == seq_step:
                            loops[12+i].is_playing = True
                        else:
                            loops[12+i].is_playing = False


        # If a Track is_waiting_rec, put it to Rec when reaches the end of length of track 0 (not initialized) or at end of selected track (if initialized)
        if self.is_waiting_rec:
            if ((self.initialized >= 1 and self.writep == self.length - 1) or
                (self.initialized == 0 and loops[0].writep == loops[0].length - 1)):
                self.is_recording = True
                self.is_waiting_rec = False
                print('---= Start Recording Track ', selected_loop, '\n')

        # If a Track is not initialized, exit and returns silence
        if self.initialized == 0:
            return(silence)

        # If the Track is initialized:
        # Control of UnMute
        if self.is_waiting_play and loops[0].writep == loops[0].length - 1:
            self.is_waiting_play = False
            self.is_playing = True

        # Control of Mute
        if self.is_waiting_mute and loops[0].writep == loops[0].length - 1:
            self.is_waiting_mute = False
            self.is_playing = False

        # If a Track is Muted or waiting_play, increment_pointers and exit with silence
        if not self.is_playing or self.is_waiting_play:
            self.increment_pointers()
            return(silence)

        # If not any of the cases, increment_pointers and return the buffer addressed by readp
        tmp = self.readp
        self.increment_pointers()

        if self.undo_mode == 1:
            return(self.main_audio[tmp, :])  # If Undo was pressed, plays only main_audio
        elif self.undo_mode == 2:
            return(self.dub_audio[tmp, :])  # If Undo was pressed, plays only dub_audio
        elif self.undo_mode == 0:
            return(self.main_audio[tmp, :]*(init_volume/max_volume)**1.4142 +
                   self.dub_audio[tmp, :]*(init_volume/max_volume)**1.4142)  # If Undo was not pressed, plays sum of main and dub audio

    # write_buffer() appends a new buffer on main_audio if not initialized or on dub_audio if initialized
    def write_buffers(self, data):
        global LENGTH
        self.maxpeak = max(np.max(np.abs(data))/max_amplitude*100, self.maxpeak)
        if self.initialized >= 1:
            if self.writep < self.length - 1:
                if self.undo_mode == 0:  # If Undo was not pressed, writes on main_audio the sum of main and dub audio
                    if self.initialized == 1:
                        self.main_audio[self.writep, :] = self.dub_audio[self.writep, :]
                    else:
                        scaled_volume = (init_volume / max_volume) ** 2
                        self.main_audio[self.writep, :] = (self.dub_audio[self.writep, :]*scaled_volume +
                                                           self.main_audio[self.writep, :]*scaled_volume)
                    self.dub_audio[self.writep, :] = np.copy(data)  # Add to dub_audio the buffer entering through Jack
                elif self.undo_mode == 1:
                    self.dub_audio[self.writep, :] = np.copy(data)  # Add to dub_audio the buffer entering through Jack
                elif self.undo_mode == 2:
                    self.main_audio[self.writep, :] = np.copy(data)  # Add to main_audio the buffer entering through Jack
            elif self.writep == self.length - 1:
                self.is_recording = False
                self.initialize()
                self.is_waiting_rec = False
                self.is_playing = True
                self.undo_mode = 0
        else:
            if self.length >= (MAXLENGTH - 1):
                self.length = 0
                print('Loop Full')
                return
            self.dub_audio[self.length, :] = np.copy(data)  # Add to main_audio the buffer entering through Jack
            self.length += 1  # Increase the length of the loop
            if not setup_donerecording:
                LENGTH += 1

    #set_recording() either starts or stops recording
    #   if uninitialized and recording, stop recording (appending) and initialize
    #   if initialized and not recording, set as "waiting to record"
    def set_recording(self):
        global setup_is_recording, setup_donerecording
        print('---= set_recording Called for Track ', selected_loop, ' =-', '\n')
        #already_recording = False

        #if chosen track is currently recording, flag it
        if self.is_recording:  # turn off recording
            self.initialize()
            self.is_playing = True
            self.is_recording = False
            self.is_waiting_rec = False
            print('-------------= Stop Rec =---', '\n')
            if selected_loop == 0 and not setup_donerecording:
                setup_is_recording = False
                setup_donerecording = True
                print('-------------= Master Track Recorded =---', '\n')
            debug()
            return
        else:
            #unless flagged, schedule recording. If chosen track was recording, then stop recording
            #if not already_recording:
            if self.is_waiting_rec and setup_donerecording:
                self.is_waiting_rec = False
                return
            if selected_loop == 0 and not setup_donerecording:
                self.is_recording = True
                setup_is_recording = True
            else:
                self.is_waiting_rec = True
            debug()

    #initialize() raises self.length to closest integer multiple of LENGTH and initializes read and write pointers
    def initialize(self): #It initializes when recording of loop stops. It de-initializes after Clearing.
        if self.initialized == 0:
            self.writep = self.length - 1
            self.length_factor = (int((self.length - OVERSHOOT) / LENGTH) + 1)
            self.length = self.length_factor * LENGTH
            self.readp = (self.writep + LATENCY) % self.length  #audio should be written ahead of where it is being read from, to compensate for input+output init.LATENCY
        self.initialized += 1
        print('     length ' + str(self.length),' /  last buffer recorded ' + str(self.writep),'\n')
        print('-------------= Initialized = ', str(self.initialized), '\n')

        # Apply Fades to dub_audio recently recorded
        np.multiply(self.dub_audio[0], fade_in, out = self.dub_audio[0], casting = 'unsafe')  # Fade In to the first buffer
        np.multiply(self.dub_audio[self.length-1], fade_out, out=self.dub_audio[self.length-1], casting='unsafe')  # Fade Out to the last buffer
        debug()

    def toggle_mute(self):
        print('-=Toggle Mute=-','\n')
        if self.initialized >= 1:
            if self.is_playing:
                if not self.is_waiting_mute:
                    self.is_waiting_mute = True
                else:
                    self.is_waiting_mute = False
                print('-------------= Mute =---', '\n')

            else:
                if not self.is_waiting_play:
                    self.is_waiting_play = True
                else:
                    self.is_waiting_play = False
                self.is_solo = False
                print('-------------= UnMute =---', '\n')
            debug()

    def toggle_solo(self):
        global seq_step
        if self.initialized >= 1:
            if not selected_loop == 12:
                print('-=Toggle Solo=-','\n')
                if not self.is_solo:
                    for i in range(number_of_tracks):
                        if i != selected_loop and loops[i].initialized >= 1 and not loops[i].is_solo and loops[i].is_playing:
                            loops[i].is_waiting_mute = True
                    self.is_solo = True
                    print('-------------= Solo =---', '\n')
                else:
                    for i in range (number_of_tracks):
                        if i != selected_loop and loops[i].initialized >= 1:
                            loops[i].is_waiting_play = True
                            loops[i].is_solo = False
                    self.is_solo = False
                    print('-------------= UnSolo =---', '\n')
            else:
                if not loops[12].is_sequence:
                    loops[12].is_sequence = True
                    self.sequence()
                    print('-------------= Sequence =---', '\n')
                else:
                    loops[12].is_sequence = False
                    print('-------------= UnSequence =---', '\n')
        debug()


    def sequence(self):
        global seq_counter, seq_step
        seq_counter = 0
        for i in range (12,16):
            if loops[i].initialized >= 1:
                seq_counter += 1
        seq_step = seq_counter - 1
        seq_step = (seq_step + 1) % seq_counter


    def undo(self):
        global LENGTH
        if self.is_recording:
            if self.initialized == 0:
                self.clear_track()
                if selected_loop == 0:
                    LENGTH = 0
                return
        if self.is_playing:
            if self.undo_mode <= 1:
                self.undo_mode += 1
            else:
                self.undo_mode = 0

        print('-=Undo=-', '\n')
        debug()

    # Clears all the Tracks of the looper
    def clear(self):
        global setup_donerecording, setup_is_recording, LENGTH
        if selected_loop == 0:
            for loop in loops:
                loop.__init__()
            setup_donerecording = False
            setup_is_recording = False
            LENGTH = 0
            print('-=Cleared ALL=-','\n')
        else:
            self.clear_track()
        debug()

    # Clears the track so that a new loop of the same or a different length can be recorded on the track
    def clear_track(self):
        self.__init__()
        print('-=Clear Track=-', '\n')

# Defining number_of_tracks of audio loops. loops[0] is the master loop.
loops = [audioloop() for _ in range(number_of_tracks)]

# Audio Processing Callback
def looping_callback(frames):
    global play_buffer, current_rec_buffer, output_volume, previous_scaling_factor

    # Comprobación de seguridad: si el puerto no está listo, salimos
    try:
        input_port
    except NameError:
        return

    # Setup: First Recording
    if not setup_donerecording:  # If setup is not done i.e. if the master loop hasn't been recorded to yet
        loops[0].is_waiting_rec = 1

    # Read input buffer from JACK
    current_rec_buffer = float2pcm(input_port.get_array())  # Capture current input jack buffer after converting Float2PCM

    # If a loop is recording, check initialization and accordingly append
    for loop in loops:
        if loop.is_recording:
            print('--------------------------------------------------=Recording=-', end='\r')
            loop.write_buffers(current_rec_buffer)

    # Converts the volumes and buffers into NumPy arrays for vectorized operations
    buffers = np.array([loop.read_buffer().astype(np.int32) for loop in loops])
    volumes = np.array([(loop.volume / max_volume) ** 2 for loop in loops], dtype=np.float32)
    # Multiply each buffer by the own volume and sum them all
    mixed_buffer = np.sum(buffers * volumes[:, None], axis=0)

    # Add to play_buffer the sum of each audio signal times the each own volume
    play_buffer[:] = np.multiply(mixed_buffer, output_volume, out=None, casting='unsafe').astype(np.int16)

    if rec_file:
        audio_buffer.write(play_buffer[:].tobytes())

    # Play mixed audio and move on to next iteration
    output_port.get_array()[:] = pcm2float(play_buffer[:])

client.set_process_callback(looping_callback)

@client.set_shutdown_callback
def shutdown(status, reason):
    print("JACK shutdown:", reason, status)

input_port = client.inports.register("input_1")
output_port = client.outports.register("output_1")
print('----- Jack Client Ports Registered -----')
print(f"In: {input_port}\nOut: {output_port}\n")

with client:
    try:
        all_captures_to_input()
        output_to_all_playbacks()
        conectar_grabacion_paralela()

        # Get MIDI Capture Ports
        outMIDIports = client.get_ports(is_midi=True, is_output=True)
        print("MIDI Capture Ports:",'\n')
        print(outMIDIports,'\n')

        # Get MIDI Playback Ports
        inMIDIports = client.get_ports(is_midi=True, is_input=True)
        print("MIDI Playback Ports:",'\n')
        print(inMIDIports,'\n')

        # If a MIDI Capture Port exists, Load FluidSynth
        target_port = 'system:midi_capture_1'
        if any(port.name == target_port for port in outMIDIports) and len(sf2_list) > 0:
            fs = fluidsynth.Synth()  # Loads FluidSynth but remains inactive
            fs.setting("audio.driver", 'jack')
            fs.setting("midi.driver", 'jack')
            fs.setting("synth.sample-rate", float(RATE))
            fs.setting("audio.jack.autoconnect", True)
            fs.setting("midi.autoconnect", False)
            fs.setting("synth.gain", 0.9)
            fs.setting("synth.cpu-cores", 4)
            print('---FluidSynth Jack Loading---', '\n')
            # Start FluidSynth
            fs.start()
            synth_initialized = True
            connect_fluidsynth()
            sleep(0.5)
            # Loads the first soundfont of the list
            sfid = fs.sfload("./sf2/" + sf2_list[0])
            fs.program_select(0, sfid, 0, 0)
            print('----- Bank: ', str(Bank), ' - ', str(sf2_list[Bank]),' / Preset: ',  ' - ', str(Preset), '\n')

        #then we turn on Green and Red lights of REC Button to indicate that looper is ready to start looping
        print("Jack Client Active. Press Ctrl+C to Stop.",'\n')

        #once all LEDs are on, we wait for the master loop record button to be pressed
        print('---Waiting for Record Button---','\n')
        debug()
        
        while True:
            show_status()
            sleep(0.1)
            pass  # Keep Client executing

    except KeyboardInterrupt:
        print("\nCerrando...")
        TurningOff()
    finally:
        Closing_Jack_Client()

# ----------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------
