"""
Sound system for firework explosion effects.
"""

import threading
import time
from typing import Optional, List, Dict

import numpy as np

# Sound support - will be enabled if sounddevice is available
try:
    import sounddevice as sd

    SOUND_AVAILABLE = True
except ImportError:
    SOUND_AVAILABLE = False


class SoundManager:
    """Manages sound generation and playback for fireworks."""

    def __init__(self):
        """Initialize sound system and generate sound effects."""
        self.enabled = SOUND_AVAILABLE
        self.explosion_sound_data: Optional[np.ndarray] = None
        self.sample_rate = 22050
        self.duration = 1.5  # Sound duration in seconds
        self.max_concurrent_sounds = 8

        # List of currently playing sounds
        self.active_sounds: List[List] = []
        self.sound_lock = threading.Lock()

        # Cache of pre-generated stereo sounds
        self.stereo_cache: Dict[float, np.ndarray] = {}

        # Thread control
        self.running = False
        self.sound_thread: Optional[threading.Thread] = None

        if self.enabled:
            try:
                self.explosion_sound_data = self._generate_explosion_sound()
                self._generate_stereo_cache()
                self._start_sound_thread()
            except Exception:
                self.enabled = False
                self.explosion_sound_data = None
                self.stereo_cache = {}

    def _generate_explosion_sound(self) -> Optional[np.ndarray]:
        """Generate a muffled, distant explosion sound.

        Returns:
            Mono audio data as numpy array (float32), or None if disabled
        """
        if not self.enabled:
            return None

        try:
            duration = self.duration
            t = np.linspace(0, duration, int(self.sample_rate * duration))

            # Generate white noise
            noise = np.random.normal(0, 1, len(t))

            # Add low-frequency rumble
            rumble_freq = 60
            rumble = np.sin(2 * np.pi * rumble_freq * t)
            rumble += 0.5 * np.sin(2 * np.pi * rumble_freq * 2 * t)

            # Combine noise and rumble
            wave = 0.7 * noise + 0.3 * rumble

            # Apply sharp attack and exponential decay envelope
            envelope = np.exp(-3 * t / duration)
            wave *= envelope

            # Add crackle
            crackle = np.random.choice([0, 1], size=len(t), p=[0.95, 0.05])
            crackle = crackle * np.random.normal(0, 0.5, len(t))
            wave += crackle * envelope

            # Apply low-pass filter using moving average
            window_size = 15
            kernel = np.ones(window_size) / window_size
            wave_filtered = np.convolve(wave, kernel, mode="same")

            # Normalize and reduce volume
            wave_filtered = np.clip(wave_filtered, -1, 1) * 0.5

            return wave_filtered.astype(np.float32)
        except Exception:
            self.enabled = False
            return None

    def _generate_stereo_cache(self):
        """Pre-generate stereo variations at different pan positions."""
        if not self.enabled or self.explosion_sound_data is None:
            return

        try:
            for i in range(17):
                pan = -1.0 + (i * 0.125)

                # Calculate left and right channel gains
                pan_angle = (pan + 1) * (np.pi / 4)
                left_gain = np.cos(pan_angle)
                right_gain = np.sin(pan_angle)

                # Create stereo audio
                mono_data = self.explosion_sound_data
                stereo_data = np.column_stack(
                    (mono_data * left_gain, mono_data * right_gain)
                ).astype(np.float32)

                self.stereo_cache[round(pan, 3)] = stereo_data
        except Exception:
            self.enabled = False
            self.stereo_cache = {}

    def _start_sound_thread(self):
        """Start the dedicated sound playback thread."""
        try:
            self.running = True
            self.sound_thread = threading.Thread(
                target=self._sound_playback_loop, daemon=True
            )
            self.sound_thread.start()
        except Exception:
            self.enabled = False
            self.running = False

    def _audio_callback(self, outdata, frames, time_info, status):
        """Callback to fill audio buffer with mixed sounds."""
        try:
            outdata.fill(0)

            with self.sound_lock:
                sounds_to_remove = []

                for i, (sound_data, position) in enumerate(self.active_sounds):
                    remaining = len(sound_data) - position
                    to_output = min(remaining, frames)

                    outdata[:to_output] += sound_data[position : position + to_output]
                    self.active_sounds[i][1] += to_output

                    if position + to_output >= len(sound_data):
                        sounds_to_remove.append(i)

                for i in reversed(sounds_to_remove):
                    self.active_sounds.pop(i)
        except Exception:
            outdata.fill(0)

    def _sound_playback_loop(self):
        """Main loop for sound playback thread."""
        if not self.enabled:
            return

        try:
            stream = sd.OutputStream(
                samplerate=self.sample_rate,
                channels=2,
                dtype="float32",
                blocksize=1024,
                callback=self._audio_callback,
            )
            stream.start()

            while self.running:
                time.sleep(0.1)

            stream.stop()
            stream.close()
        except Exception:
            self.enabled = False

    def stop(self):
        """Stop the sound playback thread."""
        try:
            self.running = False
            if self.sound_thread:
                self.sound_thread.join(timeout=1.0)
        except Exception:
            pass

    def play_explosion(self, x: float, screen_width: float):
        """Play the explosion sound with stereo panning.

        Args:
            x: Horizontal position of the explosion
            screen_width: Width of the screen
        """
        if not self.enabled or not self.stereo_cache:
            return

        try:
            with self.sound_lock:
                if len(self.active_sounds) >= self.max_concurrent_sounds:
                    return

            # Calculate pan position
            pan = (x / screen_width) * 2 - 1
            pan = np.clip(pan, -1, 1)

            # Quantize pan to nearest cached position
            quantized_pan = round(pan / 0.125) * 0.125
            quantized_pan = round(np.clip(quantized_pan, -1.0, 1.0), 3)

            stereo_data = self.stereo_cache.get(quantized_pan)
            if stereo_data is None:
                stereo_data = self.stereo_cache.get(0.0)
                if stereo_data is None:
                    return

            with self.sound_lock:
                self.active_sounds.append([stereo_data, 0])
        except Exception:
            pass
