import numpy as np
import wave
import sounddevice as sd
from scipy.signal import butter, lfilter, correlate, resample
from scipy.signal.windows import hann
from datetime import datetime

class Modem:
    SAMPLE_RATE = 44100  # Frequenza di campionamento in Hz
    BIT_DURATION = 0.1  # Durata del segnale per bit
    BIT_SEPARATION = 0.01  # Separazione tra i segnali dei bit
    FREQ_0 = 1000   # Frequenza dello 0
    FREQ_1 = 2000   # Frequenza dell'1
    AMPLITUDE = 0.9 # Ampiezza (volume) 
    SYNC_SEQUENCE = "AAAAAAAAA"  # Sequenza di sync
    TRANSITION_TYPE = 'hard'
    
    def __init__(self):
        self.samples_per_bit = int(self.SAMPLE_RATE * self.BIT_DURATION)
        # Pre-calcola i filtri per le frequenze dei bit
        self.b_0, self.a_0 = self.butter_bandpass(self.FREQ_0 - 100, self.FREQ_0 + 100, self.SAMPLE_RATE)
        self.b_1, self.a_1 = self.butter_bandpass(self.FREQ_1 - 100, self.FREQ_1 + 100, self.SAMPLE_RATE)

    def butter_bandpass(self, lowcut, highcut, fs, order=4):
        """
        Crea un filtro passa-banda Butterworth. 
        Usa le funzioni scipy.signal.butter per creare i coefficienti del filtro passa-banda.
        Normalizza le frequenze di taglio rispetto alla frequenza di Nyquist (fs/2).
        
        Parameters:
            lowcut (float): Frequenza di taglio inferiore in Hz
            highcut (float): Frequenza di taglio superiore in Hz
            fs (float): Frequenza di campionamento in Hz
            order (int, optional): Ordine del filtro. Default è 4
            
        Returns:
            tuple(ndarray, ndarray): Coefficienti del filtro (b, a) dove:
                - b: numeratore del filtro (numpy array)
                - a: denominatore del filtro (numpy array)
                Questi coefficienti sono usati da scipy.signal.lfilter
        """
        nyquist = 0.5 * fs
        low = lowcut / nyquist
        high = highcut / nyquist
        b, a = butter(order, [low, high], btype='band')
        return b, a
    
    def text_to_signal(self, text):
        """
        Converte una stringa di testo in un segnale audio utilizzando operazioni bitwise.
        Ogni carattere è convertito in 8 bit, e ogni bit è modulato in una frequenza.
        Se TRANSITION_TYPE è 'smooth', applica una finestra di Hann per transizioni smooth.
        Se TRANSITION_TYPE è 'hard', non applica la finestra di Hann e aggiunge una pausa
        di durata BIT_SEPARATION tra i bit.
        """
        signal = []
        samples_for_separation = int(self.SAMPLE_RATE * self.BIT_SEPARATION)  # Campioni per la pausa
        
        for char in text:
            # Converti il carattere in un byte (8 bit)
            byte = ord(char)
            for i in range(7, -1, -1):  # Itera sui bit da MSB a LSB
                bit = (byte >> i) & 1  # Estrai il bit i-esimo
                freq = self.FREQ_1 if bit == 1 else self.FREQ_0
                t = np.linspace(0, self.BIT_DURATION, self.samples_per_bit, endpoint=False)
                
                # Genera l'onda
                if self.TRANSITION_TYPE == 'smooth':
                    # Applica la finestra di Hann per transizioni smooth
                    window = hann(len(t))
                    wave_bit = self.AMPLITUDE * np.sin(2 * np.pi * freq * t) * window
                else:  # TRANSITION_TYPE == 'hard'
                    # Genera l'onda senza finestra di Hann
                    wave_bit = self.AMPLITUDE * np.sin(2 * np.pi * freq * t)
                
                signal.extend(wave_bit)
                
                # Aggiungi una pausa (silenzio) dopo ogni bit se TRANSITION_TYPE è 'hard'
                if self.TRANSITION_TYPE == 'hard':
                    signal.extend(np.zeros(samples_for_separation))
        
        return np.array(signal, dtype=np.float32)

    def text_to_signal_byte(self, text):
        """
        Converte una stringa di testo in un segnale audio modulando ogni byte come una singola frequenza.
        - Se TRANSITION_TYPE è 'smooth', applica una finestra di Hann per transizioni smooth.
        - Se TRANSITION_TYPE è 'hard', non applica la finestra di Hann e aggiunge una pausa
        di durata BIT_SEPARATION tra i byte.
        """
        signal = []
        samples_for_separation = int(self.SAMPLE_RATE * self.BIT_SEPARATION)  # Campioni per la pausa
        
        for char in text:
            # Converti il carattere in un byte (8 bit)
            byte = ord(char)
            
            # Calcola la frequenza proporzionale al valore del byte
            freq = self.FREQ_0 + (byte / 255) * (self.FREQ_1 - self.FREQ_0)
            
            # Genera il segnale per il byte
            t = np.linspace(0, self.BIT_DURATION * 8, self.samples_per_bit * 8, endpoint=False)  # Durata totale per il byte
            
            if self.TRANSITION_TYPE == 'smooth':
                # Applica la finestra di Hann per transizioni smooth
                window = hann(len(t))
                wave_byte = self.AMPLITUDE * np.sin(2 * np.pi * freq * t) * window
            else:  # TRANSITION_TYPE == 'hard'
                # Genera l'onda senza finestra di Hann
                wave_byte = self.AMPLITUDE * np.sin(2 * np.pi * freq * t)
            
            signal.extend(wave_byte)
            
            # Aggiungi una pausa (silenzio) dopo ogni byte se TRANSITION_TYPE è 'hard'
            if self.TRANSITION_TYPE == 'hard':
                signal.extend(np.zeros(samples_for_separation))
        
        return np.array(signal, dtype=np.float32)

    def play(self, text):
        """
        Suona il segnale audio generato dal testo sul dispositivo predefinito.
        """
        signal = self.text_to_signal(text)
        sd.play(signal, self.SAMPLE_RATE)
        sd.wait()  # Attendi la fine della riproduzione

    def save(self, text):
        """
        Salva il segnale audio generato dal testo in un file WAV.
        Il nome del file è generato automaticamente con la data e l'ora corrente.
        """
        signal = self.text_to_signal(text)
        filename = "modem_" + datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".wav"
        
        # Normalizza il segnale per evitare clipping
        signal_int = np.int16(signal * 32767)
        
        # Salva il file WAV
        with wave.open(filename, 'wb') as wf:
            wf.setnchannels(1)  # Mono
            wf.setsampwidth(2)  # 16 bit
            wf.setframerate(self.SAMPLE_RATE)
            wf.writeframes(signal_int.tobytes())
        print(f"File salvato come {filename}")
        
modem = Modem()
modem.save("Ciao")