import numpy as np
import wave
import os
from datetime import datetime
from PyQt5.QtCore import QObject, pyqtSignal
import struct

class AudioProcessor(QObject):
    processing_complete = pyqtSignal(str)  # filepath
    
    def __init__(self):
        super().__init__()
        self.debug_callback = None
        
    def debug(self, message):
        if self.debug_callback:
            self.debug_callback(f"[AudioProcessor] {message}")
        else:
            print(f"[AudioProcessor] {message}")
    
    def set_debug_callback(self, callback):
        self.debug_callback = callback
    
    def apply_gain(self, audio_data, gain_db):
        """Apply gain to audio data"""
        # Convert bytes to numpy array
        samples = np.frombuffer(audio_data, dtype=np.int16)
        
        # Apply gain
        gain_linear = 10 ** (gain_db / 20.0)
        samples_float = samples.astype(np.float32) * gain_linear
        
        # Clip to prevent overflow
        samples_float = np.clip(samples_float, -32768, 32767)
        
        # Convert back to int16
        return samples_float.astype(np.int16).tobytes()
    
    def apply_noise_gate(self, audio_data, threshold_db=-40):
        """Apply noise gate to remove low-level noise"""
        samples = np.frombuffer(audio_data, dtype=np.int16)
        
        # Calculate RMS in dB
        rms = np.sqrt(np.mean(samples.astype(np.float32) ** 2))
        rms_db = 20 * np.log10(max(rms, 1e-10))
        
        # Gate the audio if below threshold
        if rms_db < threshold_db:
            return np.zeros_like(samples, dtype=np.int16).tobytes()
        
        return audio_data
    
    def apply_low_pass_filter(self, audio_data, cutoff_hz=3400, sample_rate=8000):
        """Apply simple low-pass filter"""
        samples = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32)
        
        # Simple moving average filter
        # Calculate filter length based on cutoff frequency
        filter_length = int(sample_rate / cutoff_hz)
        if filter_length < 3:
            filter_length = 3
            
        # Apply moving average
        filtered = np.convolve(samples, np.ones(filter_length) / filter_length, mode='same')
        
        return filtered.astype(np.int16).tobytes()
    
    def apply_high_pass_filter(self, audio_data, cutoff_hz=300, sample_rate=8000):
        """Apply simple high-pass filter"""
        samples = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32)
        
        # Simple differentiator as high-pass
        filtered = np.diff(samples, prepend=samples[0])
        
        # Scale to maintain amplitude
        scale = cutoff_hz / (sample_rate / 2)
        filtered *= scale
        
        return filtered.astype(np.int16).tobytes()
    
    def normalize_audio(self, audio_data, target_db=-3):
        """Normalize audio to target dB level"""
        samples = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32)
        
        # Find peak
        peak = np.max(np.abs(samples))
        if peak == 0:
            return audio_data
        
        # Calculate current peak in dB
        current_db = 20 * np.log10(peak / 32768.0)
        
        # Calculate gain needed
        gain_db = target_db - current_db
        
        # Apply gain
        return self.apply_gain(audio_data, gain_db)
    
    def remove_silence(self, audio_data, threshold_db=-40, min_silence_ms=100, sample_rate=8000):
        """Remove silence from audio"""
        samples = np.frombuffer(audio_data, dtype=np.int16)
        
        # Calculate frame size for silence detection
        frame_size = int(sample_rate * min_silence_ms / 1000)
        
        # Detect non-silent regions
        non_silent_regions = []
        i = 0
        
        while i < len(samples):
            frame = samples[i:i+frame_size]
            if len(frame) == 0:
                break
                
            # Calculate RMS of frame
            rms = np.sqrt(np.mean(frame.astype(np.float32) ** 2))
            rms_db = 20 * np.log10(max(rms, 1e-10))
            
            if rms_db > threshold_db:
                # Found non-silent region, find its extent
                start = i
                while i < len(samples):
                    frame = samples[i:i+frame_size]
                    if len(frame) == 0:
                        break
                    rms = np.sqrt(np.mean(frame.astype(np.float32) ** 2))
                    rms_db = 20 * np.log10(max(rms, 1e-10))
                    if rms_db <= threshold_db:
                        break
                    i += frame_size
                non_silent_regions.append((start, i))
            else:
                i += frame_size
        
        # Combine non-silent regions
        if not non_silent_regions:
            return audio_data  # Return original if all silent
        
        combined = []
        for start, end in non_silent_regions:
            combined.extend(samples[start:end])
        
        return np.array(combined, dtype=np.int16).tobytes()
    
    def save_processed_audio(self, audio_data, original_path, processing_type):
        """Save processed audio with descriptive filename"""
        # Generate new filename
        base_name = os.path.splitext(os.path.basename(original_path))[0]
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        new_filename = f"{base_name}_{processing_type}_{timestamp}.wav"
        
        # Ensure directory exists
        save_dir = os.path.dirname(original_path)
        if not save_dir:
            save_dir = "wav"
        os.makedirs(save_dir, exist_ok=True)
        
        save_path = os.path.join(save_dir, new_filename)
        
        try:
            with wave.open(save_path, 'wb') as wav_file:
                wav_file.setnchannels(1)
                wav_file.setsampwidth(2)
                wav_file.setframerate(8000)
                wav_file.writeframes(audio_data)
            
            self.debug(f"Saved processed audio to {save_path}")
            self.processing_complete.emit(save_path)
            return save_path
            
        except Exception as e:
            self.debug(f"Failed to save processed audio: {e}")
            return None
    
    def concatenate_audio_files(self, file_paths, output_path=None):
        """Concatenate multiple audio files"""
        if not file_paths:
            return None
        
        combined_data = b''
        sample_rate = None
        
        for file_path in file_paths:
            try:
                with wave.open(file_path, 'rb') as wav_file:
                    if sample_rate is None:
                        sample_rate = wav_file.getframerate()
                    elif wav_file.getframerate() != sample_rate:
                        self.debug(f"Sample rate mismatch in {file_path}")
                        continue
                    
                    data = wav_file.readframes(wav_file.getnframes())
                    combined_data += data
                    
            except Exception as e:
                self.debug(f"Failed to read {file_path}: {e}")
        
        if not combined_data:
            return None
        
        # Save concatenated audio
        if not output_path:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            output_path = f"wav/concatenated_{timestamp}.wav"
        
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        try:
            with wave.open(output_path, 'wb') as wav_file:
                wav_file.setnchannels(1)
                wav_file.setsampwidth(2)
                wav_file.setframerate(sample_rate or 8000)
                wav_file.writeframes(combined_data)
            
            self.debug(f"Saved concatenated audio to {output_path}")
            return output_path
            
        except Exception as e:
            self.debug(f"Failed to save concatenated audio: {e}")
            return None