#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import argparse
import sys
import time

# Impostazioni di connessione predefinite
DEFAULT_HOST = "192.168.1.62"
DEFAULT_PORT = 49280

# Configurazioni specifiche per Yamaha TF5
TF5_INPUT_CHANNELS = 40  # 32 locali + 8 altro
TF5_MIX_BUSSES = 20      # 20 Aux/Mix

def create_connection(host, port):
    """Crea e restituisce un socket connesso."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((host, port))
        return s
    except socket.error as e:
        print(f"Errore critico di connessione a {host}:{port} -> {e}")
        sys.exit(1)

def send_command(host, port, command):
    """Invia un comando singolo e stampa la risposta (modalità one-shot)."""
    s = create_connection(host, port)
    try:
        s.sendall((command + '\n').encode('utf-8'))
        print(f"Comando inviato: '{command}'")
        response = s.recv(4096)
        print(f"Risposta: {response.decode('utf-8', errors='ignore').strip()}")
    finally:
        s.close()

def get_param(sock, command):
    """Funzione helper per ottenere un singolo parametro mantenendo il socket aperto."""
    try:
        sock.sendall((command + '\n').encode('utf-8'))
        # Riceviamo dati finché non troviamo un newline, per essere sicuri di avere la risposta intera
        response = b""
        while b'\n' not in response:
            chunk = sock.recv(1024)
            if not chunk: break
            response += chunk
        return response.decode('utf-8', errors='ignore').strip()
    except Exception as e:
        return f"Error: {e}"

def parse_val(response):
    """Estrae l'ultimo valore numerico da una risposta standard 'OK ... value'."""
    parts = response.split()
    if len(parts) > 0 and parts[0] == "OK":
        return parts[-1]
    return "N/A"

def parse_name(response):
    """Estrae il nome tra virgolette dalla risposta."""
    # Esempio risposta: OK MIXER:Current/InCh/Label/Name 0 0 "Kick Drum"
    try:
        start = response.find('"') + 1
        end = response.rfind('"')
        if start > 0 and end > start:
            return response[start:end]
        return "Unknown"
    except:
        return "Error"

def format_db(val_str):
    """Converte il valore interno (es. 1000) in stringa dB (es. +10.0 dB)."""
    try:
        val = int(val_str)
        if val <= -32768:
            return "-inf dB"
        return f"{val / 100.0:+.1f} dB"
    except:
        return val_str

def format_pan(val_str):
    """Converte il valore pan (-63 a 63) in formato leggibile (L63, C, R63)."""
    try:
        val = int(val_str)
        if val == 0: return "C"
        if val < 0: return f"L{abs(val)}"
        return f"R{val}"
    except:
        return val_str

def get_full_config(host, port):
    """Scarica e stampa la configurazione di tutti i canali e mix."""
    print(f"Connessione a {host}... Inizio download configurazione.")
    print("Attendere prego, l'operazione potrebbe richiedere alcuni secondi...\n")

    s = create_connection(host, port)
    # Impostiamo un timeout breve per le singole richieste per velocizzare eventuali errori
    s.settimeout(2)

    try:
        # --- INPUT CHANNELS ---
        print("-" * 65)
        print(f"{'CH':<4} {'NAME':<16} {'STATUS':<8} {'FADER':<12} {'PAN':<8}")
        print("-" * 65)
        
        for i in range(TF5_INPUT_CHANNELS):
            # Recupera Nome
            resp_name = get_param(s, f"get MIXER:Current/InCh/Label/Name {i} 0")
            name = parse_name(resp_name)
            
            # Recupera ON/OFF
            resp_on = get_param(s, f"get MIXER:Current/InCh/Fader/On {i} 0")
            is_on = parse_val(resp_on)
            status = " [ON] " if is_on == "1" else " OFF "
            
            # Recupera Livello Fader
            resp_lvl = get_param(s, f"get MIXER:Current/InCh/Fader/Level {i} 0")
            level_db = format_db(parse_val(resp_lvl))

            # Recupera Pan
            resp_pan = get_param(s, f"get MIXER:Current/InCh/ToSt/Pan {i} 0")
            pan_str = format_pan(parse_val(resp_pan))

            print(f"CH{i+1:<2} {name[:15]:<16} {status:<8} {level_db:<12} {pan_str:<8}")
            # Piccolo sleep per non saturare il buffer del mixer se necessario
            # time.sleep(0.01) 

        print("\n")
        # --- MIX (AUX) BUSSES ---
        print("-" * 50)
        print(f"{'MIX':<4} {'NAME':<16} {'STATUS':<8} {'FADER':<12}")
        print("-" * 50)

        for i in range(TF5_MIX_BUSSES):
             # Recupera Nome Mix
            resp_name = get_param(s, f"get MIXER:Current/Mix/Label/Name {i} 0")
            name = parse_name(resp_name)

             # Recupera ON/OFF Mix
            resp_on = get_param(s, f"get MIXER:Current/Mix/Fader/On {i} 0")
            is_on = parse_val(resp_on)
            status = " [ON] " if is_on == "1" else " OFF "

            # Recupera Livello Mix
            resp_lvl = get_param(s, f"get MIXER:Current/Mix/Fader/Level {i} 0")
            level_db = format_db(parse_val(resp_lvl))

            print(f"MX{i+1:<2} {name[:15]:<16} {status:<8} {level_db:<12}")

        print("-" * 50)
        print("\nConfigurazione completata.")

    except KeyboardInterrupt:
        print("\nOperazione interrotta dall'utente.")
    except Exception as e:
        print(f"\nErrore durante il recupero della configurazione: {e}")
    finally:
        s.close()

def get_full_config_dict(host, port):
    """
    Scarica la configurazione di tutti i canali e mix e la restituisce come dizionario.
    """
    print(f"Connessione a {host}... Inizio download configurazione.")
    print("Attendere prego, l'operazione potrebbe richiedere alcuni secondi...\n")

    s = create_connection(host, port)
    # Impostiamo un timeout breve per le singole richieste per velocizzare eventuali errori
    s.settimeout(2)

    config = {
        "input_channels": [],
        "mix_busses": []
    }

    try:
        # --- INPUT CHANNELS ---
        for i in range(TF5_INPUT_CHANNELS):
            channel_data = {}
            
            # Recupera Nome
            resp_name = get_param(s, f"get MIXER:Current/InCh/Label/Name {i} 0")
            channel_data['name'] = parse_name(resp_name)
            
            # Recupera ON/OFF
            resp_on = get_param(s, f"get MIXER:Current/InCh/Fader/On {i} 0")
            channel_data['status'] = 'ON' if parse_val(resp_on) == "1" else 'OFF'
            
            # Recupera Livello Fader
            resp_lvl = get_param(s, f"get MIXER:Current/InCh/Fader/Level {i} 0")
            channel_data['fader_db'] = format_db(parse_val(resp_lvl))

            # Recupera Pan
            resp_pan = get_param(s, f"get MIXER:Current/InCh/ToSt/Pan {i} 0")
            channel_data['pan'] = format_pan(parse_val(resp_pan))
            
            config["input_channels"].append(channel_data)

        # --- MIX (AUX) BUSSES ---
        for i in range(TF5_MIX_BUSSES):
            mix_data = {}

            # Recupera Nome Mix
            resp_name = get_param(s, f"get MIXER:Current/Mix/Label/Name {i} 0")
            mix_data['name'] = parse_name(resp_name)

            # Recupera ON/OFF Mix
            resp_on = get_param(s, f"get MIXER:Current/Mix/Fader/On {i} 0")
            mix_data['status'] = 'ON' if parse_val(resp_on) == "1" else 'OFF'

            # Recupera Livello Mix
            resp_lvl = get_param(s, f"get MIXER:Current/Mix/Fader/Level {i} 0")
            mix_data['fader_db'] = format_db(parse_val(resp_lvl))
            
            config["mix_busses"].append(mix_data)

        print("Configurazione scaricata con successo.")
        return config

    except KeyboardInterrupt:
        print("\nOperazione interrotta dall'utente.")
        return None
    except Exception as e:
        print(f"\nErrore durante il recupero della configurazione: {e}")
        return None
    finally:
        s.close()

def main():
    parser = argparse.ArgumentParser(
        description='CLI per controllare un mixer Yamaha TF5.',
        formatter_class=argparse.RawTextHelpFormatter
    )
    parser.add_argument('--host', default=DEFAULT_HOST, help=f'IP del mixer (default: {DEFAULT_HOST})')
    parser.add_argument('--port', type=int, default=DEFAULT_PORT, help=f'Porta (default: {DEFAULT_PORT})')

    subparsers = parser.add_subparsers(dest='command', help='Comandi disponibili')

    # Comandi esistenti
    p_recall = subparsers.add_parser('recall', help='Richiama scena (es: recall a 0)')
    p_recall.add_argument('banco', choices=['a', 'b'])
    p_recall.add_argument('numero', type=int)

    p_level = subparsers.add_parser('level', help='Imposta fader (es: level 0 1000)')
    p_level.add_argument('canale', type=int)
    p_level.add_argument('valore', type=int)

    p_onoff = subparsers.add_parser('onoff', help='Imposta on/off (es: onoff 0 on)')
    p_onoff.add_argument('canale', type=int)
    p_onoff.add_argument('stato', choices=['on', 'off'])

    p_pan = subparsers.add_parser('pan', help='Imposta pan (es: pan 0 -63)')
    p_pan.add_argument('canale', type=int)
    p_pan.add_argument('valore', type=int)
    
    p_raw = subparsers.add_parser('raw', help='Invia comando grezzo')
    p_raw.add_argument('raw_cmd', nargs='+')

    # --- NUOVO COMANDO ---
    subparsers.add_parser('config', help='Legge e stampa la configurazione completa del mixer')
    # ---------------------

    args = parser.parse_args()

    if args.command == 'config':
        get_full_config(args.host, args.port)
    elif args.command == 'recall':
        send_command(args.host, args.port, f"ssrecall_ex scene_{args.banco} {args.numero}")
    elif args.command == 'level':
        send_command(args.host, args.port, f"set MIXER:Current/InCh/Fader/Level {args.canale} 0 {args.valore}")
    elif args.command == 'onoff':
        val = 1 if args.stato == 'on' else 0
        send_command(args.host, args.port, f"set MIXER:Current/InCh/Fader/On {args.canale} 0 {val}")
    elif args.command == 'pan':
        send_command(args.host, args.port, f"set MIXER:Current/InCh/ToSt/Pan {args.canale} 0 {args.valore}")
    elif args.command == 'raw':
        send_command(args.host, args.port, ' '.join(args.raw_cmd))
    else:
        parser.print_help()

if __name__ == '__main__':
    main()