import os
from threading import Thread, Event
from typing import Callable, Any

import numpy as np

from buzz import whisper_audio

mock_query_devices = [
    {
        "name": "Background Music",
        "index": 0,
        "hostapi": 0,
        "max_input_channels": 2,
        "max_output_channels": 2,
        "default_low_input_latency": 0.01,
        "default_low_output_latency": 0.008,
        "default_high_input_latency": 0.1,
        "default_high_output_latency": 0.064,
        "default_samplerate": 8000.0,
    },
    {
        "name": "Background Music (UI Sounds)",
        "index": 1,
        "hostapi": 0,
        "max_input_channels": 2,
        "max_output_channels": 2,
        "default_low_input_latency": 0.01,
        "default_low_output_latency": 0.008,
        "default_high_input_latency": 0.1,
        "default_high_output_latency": 0.064,
        "default_samplerate": 8000.0,
    },
    {
        "name": "BlackHole 2ch",
        "index": 2,
        "hostapi": 0,
        "max_input_channels": 2,
        "max_output_channels": 2,
        "default_low_input_latency": 0.01,
        "default_low_output_latency": 0.0013333333333333333,
        "default_high_input_latency": 0.1,
        "default_high_output_latency": 0.010666666666666666,
        "default_samplerate": 48000.0,
    },
    {
        "name": "MacBook Pro Microphone",
        "index": 3,
        "hostapi": 0,
        "max_input_channels": 1,
        "max_output_channels": 0,
        "default_low_input_latency": 0.034520833333333334,
        "default_low_output_latency": 0.01,
        "default_high_input_latency": 0.043854166666666666,
        "default_high_output_latency": 0.1,
        "default_samplerate": 48000.0,
    },
    {
        "name": "MacBook Pro Speakers",
        "index": 4,
        "hostapi": 0,
        "max_input_channels": 0,
        "max_output_channels": 2,
        "default_low_input_latency": 0.01,
        "default_low_output_latency": 0.0070416666666666666,
        "default_high_input_latency": 0.1,
        "default_high_output_latency": 0.016375,
        "default_samplerate": 48000.0,
    },
    {
        "name": "Null Audio Device",
        "index": 5,
        "hostapi": 0,
        "max_input_channels": 2,
        "max_output_channels": 2,
        "default_low_input_latency": 0.01,
        "default_low_output_latency": 0.0014512471655328798,
        "default_high_input_latency": 0.1,
        "default_high_output_latency": 0.011609977324263039,
        "default_samplerate": 44100.0,
    },
    {
        "name": "Multi-Output Device",
        "index": 6,
        "hostapi": 0,
        "max_input_channels": 0,
        "max_output_channels": 2,
        "default_low_input_latency": 0.01,
        "default_low_output_latency": 0.0033333333333333335,
        "default_high_input_latency": 0.1,
        "default_high_output_latency": 0.012666666666666666,
        "default_samplerate": 48000.0,
    },
]


class MockInputStream:
    thread: Thread
    samplerate = whisper_audio.SAMPLE_RATE

    def __init__(
        self,
        callback: Callable[[np.ndarray, int, Any, Any], None],
        *args,
        **kwargs,
    ):
        self._stop_event = Event()
        self.callback = callback

        # Pre-load audio on the calling (main) thread to avoid calling
        # subprocess.run (fork) from a background thread on macOS, which
        # can cause a segfault when Qt is running.
        sample_rate = whisper_audio.SAMPLE_RATE
        file_path = os.path.join(
            os.path.dirname(__file__), "../testdata/whisper-french.mp3"
        )
        self._audio = whisper_audio.load_audio(file_path, sr=sample_rate)

        self.thread = Thread(target=self.target)

    def start(self):
        self.thread.start()

    def target(self):
        sample_rate = whisper_audio.SAMPLE_RATE
        audio = self._audio

        chunk_duration_secs = 1

        seek = 0
        num_samples_in_chunk = chunk_duration_secs * sample_rate

        while not self._stop_event.is_set():
            self._stop_event.wait(timeout=chunk_duration_secs)
            if self._stop_event.is_set():
                break
            chunk = audio[seek : seek + num_samples_in_chunk]
            try:
                self.callback(chunk, 0, None, None)
            except RuntimeError:
                # Qt object was deleted between the stop-event check and
                # the callback invocation; treat it as a stop signal.
                break
            seek += num_samples_in_chunk

            # loop back around
            if seek + num_samples_in_chunk > audio.size:
                seek = 0

    def stop(self):
        self._stop_event.set()
        if self.thread.is_alive():
            self.thread.join(timeout=5)

    def close(self):
        self.stop()

    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()


class MockSoundDevice:
    def __init__(self):
        self.devices = mock_query_devices

    def InputStream(self, *args, **kwargs):
        return MockInputStream(*args, **kwargs)

    def query_devices(self, device=None):
        if device is None:
            return self.devices
        else:
            return next((d for d in self.devices if d['index'] == device), None)

    def check_input_settings(self, device=None, samplerate=None):
        device_info = self.query_devices(device)
        if device_info and samplerate and samplerate != device_info['default_samplerate']:
            raise ValueError('Invalid sample rate for device')