import sounddevice as sd
import numpy as np
import os
from datetime import datetime
import scipy.io.wavfile as wavfile
from metadata_manager import MetaDataManager
from audio_clip import AudioClip


# AudioClip class for clip playback


class AudioIO:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            # print("Creating new AudioRecorder instance")
            cls._instance = super().__new__(cls)
            cls._instance.init()
        return cls._instance
    def init(self):
        self.duration = 30
        self.channels = 2
        self.input_sample_rate = 44100
        self.output_sample_rate = 44100
        self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32)
        self.recordings_dir = "recordings"

        sd.default.latency = 'low'

        self.socket = None
        
        self.in_stream = sd.InputStream(
            callback=self.record_callback
        )

        self.out_stream = sd.OutputStream(
            callback=self.playback_callback,
            latency=3
        )

        self.clip_map = {}


    def refresh_streams(self):
        was_active = self.in_stream.active 
        if was_active:
            self.in_stream.stop()
            self.out_stream.stop()
        
        self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32)
        # print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}")
        self.in_stream = sd.InputStream(
            callback=self.record_callback
        )

        self.out_stream = sd.OutputStream(
            callback=self.playback_callback
        )

        if was_active:
            self.in_stream.start()
            self.out_stream.start()

        
    
    def record_callback(self, indata, frames, time, status):
        if status:
            # print(f"Recording status: {status}")
            pass
        
        # Circular buffer implementation
        self.buffer = np.roll(self.buffer, -frames, axis=0)
        self.buffer[-frames:] = indata
    
    def playback_callback(self, outdata, frames, time, status):
        if status:
            # print(f"Playback status: {status}")
            pass

        outdata.fill(0)
        
        # Iterate over a copy of the items to avoid modifying the dictionary during iteration
        for clip_id, clip_list in list(self.clip_map.items()):
            for clip in clip_list[:]:  # Iterate over a copy of the list
                if not clip.is_finished():
                    samples = clip.get_samples(frames)
                    outdata[:] += samples.reshape(-1, 1)  # Mix into output
                if clip.is_finished():
                    self.clip_map[clip_id].remove(clip)
                    if len(self.clip_map[clip_id]) == 0:
                        del self.clip_map[clip_id]
                        break  # Exit inner loop since the key is deleted
        
    
    def save_last_n_seconds(self):
        # Create output directory if it doesn't exist
        os.makedirs(self.recordings_dir, exist_ok=True)
        
        # Generate filename with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav")
        
        # Normalize audio to prevent clipping
        audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5
        
        # Convert float32 to int16 for WAV file
        audio_data_int16 = (audio_data * 32767).astype(np.int16)
        
        # Write buffer to file
        wavfile.write(filename, int(self.input_sample_rate), audio_data_int16)

        meta = MetaDataManager()

        clip_metadata = {
            "filename": filename,
            "name": f"Clip {timestamp}",
            "playbackType":"playStop",
            "volume": 1.0,  
        }
        
        meta.add_clip_to_collection("Uncategorized",  clip_metadata )
        self.socket.emit('new_clip', clip_metadata)
        
        return clip_metadata
    
    def set_buffer_duration(self, duration):
        self.duration = duration
        self.buffer = np.zeros((int(duration * self.input_sample_rate), self.channels), dtype=np.float32)

    def set_recording_directory(self, directory):
        self.recordings_dir = directory
    
    def start_recording(self):
        if(self.in_stream.active):
            # print("Already recording")
            return
        # print('number of channels', self.channels)
        
        self.in_stream.start()
        self.out_stream.start()
        self.output_sample_rate = self.out_stream.samplerate
        self.input_sample_rate = self.in_stream.samplerate  

    def stop_recording(self):
        if(not self.in_stream.active):
            # print("Already stopped")
            return
        
        self.in_stream.stop()
        self.out_stream.stop()

    def is_recording(self):
        return self.in_stream.active
    
    def play_clip(self, clip_metadata):
        print(f"Playing clip: {clip_metadata}")
        clip_id = clip_metadata.get("filename")
        if clip_metadata.get("playbackType") == "playStop":
            if clip_id in self.clip_map:
                del self.clip_map[clip_id]
                return
            else:
                self.clip_map[clip_id] = []
        if clip_id not in self.clip_map:
            self.clip_map[clip_id] = []
        self.clip_map[clip_id].append(AudioClip(clip_metadata, target_sample_rate=self.output_sample_rate))