"""
Main CLI application for real-time singing pitch detection.
"""

import sys
import time
import signal
import threading
from typing import Optional
import argparse

try:
    import pyaudio
    import numpy as np
except ImportError as e:
    print(f"Missing required dependency: {e}")
    print("Please install dependencies with: pip install pyaudio numpy scipy")
    sys.exit(1)

from .pitch_detector import PitchDetector


class SingDetectorApp:
    """Main application class for real-time pitch detection."""
    
    def __init__(self, sample_rate: int = 44100, frame_size: int = 4096, 
                 update_rate: float = 20.0):
        """
        Initialize the sing detector application.
        
        Args:
            sample_rate: Audio sample rate in Hz
            frame_size: Size of audio frame to analyze
            update_rate: Display update rate in Hz
        """
        self.sample_rate = sample_rate
        self.frame_size = frame_size
        self.update_rate = update_rate
        self.running = False
        
        # Initialize audio and pitch detector
        self.audio = None
        self.stream = None
        self.pitch_detector = PitchDetector(sample_rate, frame_size)
        
        # Threading for audio processing
        self.audio_lock = threading.Lock()
        self.latest_audio = None
        
        # Waveform visualization
        self.waveform_history = []
        self.waveform_length = 40  # Number of bars in waveform display
        
    def audio_callback(self, in_data, frame_count, time_info, status):
        """Callback function for audio input stream."""
        if status:
            print(f"Audio status: {status}")
            
        # Convert audio data to numpy array
        audio_data = np.frombuffer(in_data, dtype=np.float32)
        
        # Store latest audio data thread-safely
        with self.audio_lock:
            self.latest_audio = audio_data.copy()
            
        return (None, pyaudio.paContinue)
    
    def setup_audio(self) -> bool:
        """
        Set up audio input stream.
        
        Returns:
            True if successful, False otherwise
        """
        try:
            self.audio = pyaudio.PyAudio()
            print("\n")
            # Check if there are input devices available
            if self.audio.get_device_count() == 0:
                print("No audio devices found!")
                return False
                
            # Find default input device
            default_device = self.audio.get_default_input_device_info()
            print(f"Audio Input: {default_device['name']}")
            print()  # Add small space after audio device line
            
            # Create input stream
            self.stream = self.audio.open(
                format=pyaudio.paFloat32,
                channels=1,
                rate=self.sample_rate,
                input=True,
                frames_per_buffer=self.frame_size,
                stream_callback=self.audio_callback,
                input_device_index=default_device['index']
            )
            
            return True
            
        except Exception as e:
            print(f"Error setting up audio: {e}")
            return False
    
    def cleanup_audio(self):
        """Clean up audio resources."""
        if self.stream:
            self.stream.stop_stream()
            self.stream.close()
        if self.audio:
            self.audio.terminate()
    
    def get_audio_level(self, audio_data: np.ndarray) -> float:
        """
        Calculate the RMS (Root Mean Square) audio level for waveform visualization.
        
        Args:
            audio_data: Array of audio samples
            
        Returns:
            Audio level between 0.0 and 1.0
        """
        if audio_data is None or len(audio_data) == 0:
            return 0.0
        
        # Calculate RMS level
        rms = np.sqrt(np.mean(audio_data ** 2))
        
        # Normalize and apply some scaling for better visualization
        level = min(rms * 10, 1.0)  # Scale up and clamp to 1.0
        
        return level
    
    def create_waveform_display(self, current_level: float) -> str:
        """
        Create a moving waveform visualization.
        
        Args:
            current_level: Current audio level (0.0 to 1.0)
            
        Returns:
            String representation of the waveform
        """
        # Add current level to history
        self.waveform_history.append(current_level)
        
        # Keep only the last N values
        if len(self.waveform_history) > self.waveform_length:
            self.waveform_history.pop(0)
        
        # Create waveform bars
        bars = []
        for level in self.waveform_history:
            # Convert level to bar height (0-8 levels)
            bar_height = int(level * 8)
            
            if bar_height == 0:
                bars.append('_')
            elif bar_height <= 2:
                bars.append('▁')
            elif bar_height <= 3:
                bars.append('▂')
            elif bar_height <= 4:
                bars.append('▃')
            elif bar_height <= 5:
                bars.append('▄')
            elif bar_height <= 6:
                bars.append('▅')
            elif bar_height <= 7:
                bars.append('▆')
            else:
                bars.append('▇')
        
        # Pad with underscores if we don't have enough history yet
        while len(bars) < self.waveform_length:
            bars.insert(0, '_')
        
        return ''.join(bars)
    
    def signal_handler(self, signum, frame):
        """Handle interrupt signal to exit gracefully."""
        self.running = False
    
    def run(self, method: str = "autocorr"):
        """
        Run the real-time pitch detection.
        
        Args:
            method: Pitch detection method ('autocorr' or 'fft')
        """
        # Set up signal handler for graceful exit
        signal.signal(signal.SIGINT, self.signal_handler)
        
        # Set up audio
        if not self.setup_audio():
            return False
            
        try:
            # Start audio stream
            self.stream.start_stream()
            self.running = True
            
            # Calculate update interval
            update_interval = 1.0 / self.update_rate
            last_update = time.time()
            
            while self.running:
                current_time = time.time()
                
                # Check if it's time to update display
                if current_time - last_update >= update_interval:
                    # Get latest audio data
                    with self.audio_lock:
                        if self.latest_audio is not None:
                            audio_data = self.latest_audio.copy()
                        else:
                            audio_data = None
                    
                    if audio_data is not None and len(audio_data) == self.frame_size:
                        # Get audio level for waveform
                        audio_level = self.get_audio_level(audio_data)
                        waveform = self.create_waveform_display(audio_level)
                        
                        # Detect pitch
                        if method == "fft":
                            frequency = self.pitch_detector.detect_pitch_fft(audio_data)
                        else:
                            frequency = self.pitch_detector.detect_pitch(audio_data)
                        
                        # Display waveform and result
                        if frequency is not None:
                            note_info = self.pitch_detector.format_note_info(frequency)
                            # Clear previous lines and redraw
                            print(f"\033[2K{waveform}\n\033[2K\n\033[2K🔊 {note_info}", end="", flush=True)
                            # Move cursor back to start of waveform line
                            print("\033[2A\r", end="", flush=True)
                        else:
                            # Clear previous lines and redraw
                            print(f"\033[2K{waveform}\n\033[2K\n\033[2K🔇  Listening...", end="", flush=True)
                            # Move cursor back to start of waveform line
                            print("\033[2A\r", end="", flush=True)
                    else:
                        # Show waveform even when no audio data
                        waveform = self.create_waveform_display(0.0)
                        # Clear previous lines and redraw
                        print(f"\033[2K{waveform}\n\033[2K\n\033[2K🔇  Listening...", end="", flush=True)
                        # Move cursor back to start of waveform line
                        print("\033[2A\r", end="", flush=True)
                    
                    last_update = current_time
                
                # Small sleep to prevent busy waiting
                time.sleep(0.01)
                
        except KeyboardInterrupt:
            pass
        finally:
            print()  # Just add a newline for clean exit
            self.cleanup_audio()
            
        return True


def main():
    """Main entry point for the CLI application."""
    parser = argparse.ArgumentParser(
        description="Real-time pitch detection for singing",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  sing                    # Start with default settings
  sing --method fft       # Use FFT method for pitch detection
  sing --rate 22050       # Use 22050 Hz sample rate
  sing --update-rate 30   # Update display 30 times per second
        """
    )
    
    parser.add_argument(
        "--method", 
        choices=["autocorr", "fft"],
        default="autocorr",
        help="Pitch detection method (default: autocorr)"
    )
    
    parser.add_argument(
        "--rate",
        type=int,
        default=44100,
        help="Audio sample rate in Hz (default: 44100)"
    )
    
    parser.add_argument(
        "--frame-size",
        type=int,
        default=4096,
        help="Audio frame size for analysis (default: 4096)"
    )
    
    parser.add_argument(
        "--update-rate",
        type=float,
        default=20.0,
        help="Display update rate in Hz (default: 20.0)"
    )
    
    parser.add_argument(
        "--version",
        action="version",
        version="sing-detector 0.1.0"
    )
    
    args = parser.parse_args()
    
    # Create and run the application
    app = SingDetectorApp(
        sample_rate=args.rate,
        frame_size=args.frame_size,
        update_rate=args.update_rate
    )
    
    success = app.run(method=args.method)
    return 0 if success else 1


if __name__ == "__main__":
    sys.exit(main()) 