from logging import getLogger
from queue import Empty, Queue
from typing import Callable, Optional

import sounddevice as sd


# pylint: disable=too-few-public-methods
class AudioOutputCallback:
    """
    The ``AudioSynthOutput`` is a functor that wraps the ``sounddevice.Stream``
    callback and writes raw audio data to the audio device.
    """

    def __init__(
        self,
        *args,
        audio_queue: Queue,  # Queue[NDArray[np.number]],
        channels: int,
        blocksize: int,
        should_stop: Callable[[], bool] = lambda: False,
        is_paused: Callable[[], bool] = lambda: False,
        queue_timeout: Optional[float] = None,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self._audio_queue = audio_queue
        self._channels = channels
        self._blocksize = blocksize
        self._should_stop = should_stop
        self._is_paused = is_paused
        self._queue_timeout = queue_timeout
        self.logger = getLogger(__name__)

    def _check_status(self, frames: int, status):
        """
        Checks the current status of the audio callback and raises errors if
        the processing shouldn't continue.
        """
        if self._should_stop():
            raise sd.CallbackStop

        if not (frames == self._blocksize):
            raise AssertionError(
                (f'Received {frames} frames, expected blocksize is {self._blocksize}',)
            )

        if status.output_underflow:
            raise AssertionError('Output underflow: increase blocksize?')
        if status:
            raise AssertionError(f'Audio callback failed: {status}')

    # outdata: NDArray[np.number]
    def _audio_callback(self, outdata, frames: int, status):
        if self._is_paused():
            return

        self._check_status(frames, status)

        try:
            data = self._audio_queue.get_nowait()
        except Empty as e:
            raise (
                sd.CallbackStop
                if self._should_stop()
                else AssertionError('Buffer is empty: increase buffersize?')
            ) from e

        if data.shape[0] == 0:
            raise sd.CallbackStop

        audio_length = min(len(data), len(outdata))
        outdata[:audio_length] = data[:audio_length]

    # _ = time
    # outdata: NDArray[np.number]
    def __call__(self, outdata, frames: int, _, status):
        try:
            self._audio_callback(outdata, frames, status)
        except AssertionError as e:
            self.logger.warning(str(e))
