from logging import getLogger
import os
import stat
from threading import Event
from time import time
from typing import Iterable, List, Optional, Union

from .._model import AudioDevice, DeviceType, StreamType
from .._streams import AudioPlayer, AudioRecorder, AudioThread
from ._device import DeviceManager
from ._stream import StreamManager


class AudioManager:
    """
    The audio manager is responsible for managing multiple audio controllers and
    their access to audio resources.

    It main purpose is to act as a proxy/facade between the high-level audio
    plugin and the audio functionalities (allocating streams, managing the state
    of the player and recorder processes, etc.).
    """

    _default_signal_timeout = 2

    def __init__(
        self,
        should_stop: Event,
        input_blocksize: int,
        output_blocksize: int,
        input_device: Optional[DeviceType] = None,
        output_device: Optional[DeviceType] = None,
        queue_size: Optional[int] = None,
    ):
        """
        :param should_stop: Event to synchronize the audio manager stop.
        :param input_blocksize: Block size for the input stream.
        :param output_blocksize: Block size for the output stream.
        :param input_device: Default device to use for the input stream.
        :param output_device: Default device to use for the output stream.
        :param queue_size: Maximum size of the audio queues.
        """
        self._should_stop = should_stop
        self._device_manager = DeviceManager(
            input_device=input_device, output_device=output_device
        )

        self._stream_manager = StreamManager(device_manager=self._device_manager)
        self.logger = getLogger(__name__)
        self.input_blocksize = input_blocksize
        self.output_blocksize = output_blocksize
        self.queue_size = queue_size

    def create_player(
        self,
        device: DeviceType,
        channels: int,
        volume: float,
        infile: Optional[str] = None,
        sound: Optional[Union[dict, Iterable[dict]]] = None,
        duration: Optional[float] = None,
        sample_rate: Optional[int] = None,
        dtype: str = 'int16',
        format: Optional[str] = None,  # pylint: disable=redefined-builtin
        blocksize: Optional[int] = None,
        latency: Union[float, str] = 'high',
        stream_name: Optional[str] = None,
        start_padding: float = 0,
        end_padding: float = 0,
    ) -> AudioPlayer:
        """
        Create an audio player thread.

        :param device: Audio device to use.
        :param channels: Number of output channels.
        :param volume: Output volume, between 0 and 100.
        :param infile: File or URL to play.
        :param sound: Alternatively to a file/URL, you can play synthetic
            sounds.
        :param duration: Duration of the stream in seconds.
        :param sample_rate: Sample rate of the stream.
        :param dtype: Data type of the stream.
        :param format: Output format of the stream.
        :param blocksize: Block size of the stream.
        :param latency: Latency of the stream.
        :param stream_name: Name of the stream.
        :param start_padding: Silence, in seconds, to prepend before audio
            playback starts.
        :param end_padding: Silence, in seconds, to append before closing the
            output stream.
        """
        dev = self._device_manager.get_device(device, type=StreamType.OUTPUT)
        player = AudioPlayer.build(
            device=device,
            infile=infile,
            sound=sound,
            duration=duration,
            volume=volume,
            sample_rate=sample_rate or dev.default_samplerate,
            dtype=dtype,
            blocksize=blocksize or self.output_blocksize,
            latency=latency,
            channels=channels,
            output_format=format,
            start_padding=start_padding,
            end_padding=end_padding,
            queue_size=self.queue_size,
            should_stop=self._should_stop,
        )

        self._stream_manager.register(
            player, dev, StreamType.OUTPUT, stream_name=stream_name
        )
        return player

    def create_recorder(
        self,
        device: DeviceType,
        output_device: Optional[DeviceType] = None,
        fifo: Optional[str] = None,
        outfile: Optional[str] = None,
        duration: Optional[float] = None,
        sample_rate: Optional[int] = None,
        dtype: str = 'int16',
        blocksize: Optional[int] = None,
        latency: Union[float, str] = 'high',
        channels: int = 1,
        volume: float = 100,
        redis_queue: Optional[str] = None,
        format: str = 'wav',  # pylint: disable=redefined-builtin
        stream: bool = True,
        stream_name: Optional[str] = None,
        play_audio: bool = False,
    ) -> AudioRecorder:
        """
        Create an audio recorder thread.

        :param device: Audio device to use.
        :param output_device: Output device to use.
        :param fifo: Path to an output FIFO file to use to synchronize the audio
            to other processes.
        :param outfile: Optional output file for the recorded audio.
        :param duration: Duration of the recording in seconds.
        :param sample_rate: Sample rate of the stream.
        :param dtype: Data type of the stream.
        :param blocksize: Block size of the stream.
        :param latency: Latency of the stream.
        :param channels: Number of output channels.
        :param volume: Input volume, between 0 and 100.
        :param redis_queue: Name of the Redis queue to use.
        :param format: Format of the recorded audio.
        :param stream: Whether to stream the recorded audio.
        :param play_audio: Whether to play the recorded audio in real-time.
        :param stream_name: Name of the stream.
        """
        blocksize = blocksize or self.input_blocksize
        dev = self._device_manager.get_device(device, type=StreamType.OUTPUT)

        if fifo:
            fifo = os.path.expanduser(fifo)
            if os.path.exists(fifo) and stat.S_ISFIFO(os.stat(fifo).st_mode):
                self.logger.info('Removing previous input stream FIFO %s', fifo)
                os.unlink(fifo)

            os.mkfifo(fifo, 0o644)
            outfile = fifo
        elif outfile:
            outfile = os.path.expanduser(outfile)

        outfile = outfile or fifo or os.devnull
        recorder = AudioRecorder(
            device=(
                (
                    dev.index,
                    self._device_manager.get_device(
                        type=StreamType.OUTPUT, device=output_device
                    ).index,
                )
                if play_audio
                else dev.index
            ),
            outfile=outfile,
            duration=duration,
            sample_rate=sample_rate or dev.default_samplerate,
            dtype=dtype,
            blocksize=blocksize,
            latency=latency,
            output_format=format,
            channels=channels,
            volume=volume,
            redis_queue=redis_queue,
            stream=stream,
            audio_pass_through=play_audio,
            queue_size=self.queue_size,
            should_stop=self._should_stop,
        )

        self._stream_manager.register(
            recorder, dev, StreamType.INPUT, stream_name=stream_name
        )
        return recorder

    def get_device(
        self,
        device: Optional[DeviceType] = None,
        type: Optional[StreamType] = None,  # pylint: disable=redefined-builtin
    ) -> AudioDevice:
        """
        Proxy to ``self._device_manager.get_device``.
        """
        return self._device_manager.get_device(device=device, type=type)

    def get_devices(
        self,
        type: Optional[StreamType] = None,  # pylint: disable=redefined-builtin
    ) -> List[AudioDevice]:
        """
        Proxy to ``self._device_manager.get_devices``.
        """
        return self._device_manager.get_devices(type=type)

    def get_streams(
        self,
        device: Optional[DeviceType] = None,
        type: Optional[StreamType] = None,  # pylint: disable=redefined-builtin
        streams: Optional[Iterable[Union[str, int]]] = None,
    ) -> List[AudioThread]:
        """
        Proxy to ``self._stream_manager.get``.
        """
        return self._stream_manager.get(device=device, type=type, streams=streams)

    def stop_audio(
        self,
        device: Optional[DeviceType] = None,
        type: Optional[StreamType] = None,  # pylint: disable=redefined-builtin
        streams: Optional[Iterable[Union[str, int]]] = None,
        timeout: Optional[float] = 2,
    ):
        """
        Stops audio sessions.

        :param device: Filter by host audio device.
        :param type: Filter by stream type (input or output).
        :param streams: Filter by stream indices/names.
        :param timeout: Wait timeout in seconds.
        """
        streams_to_stop = self._stream_manager.get(device, type, streams=streams)

        # Send the stop signals
        for audio_thread in streams_to_stop:
            audio_thread.notify_stop()

        # Wait for termination (with timeout)
        wait_start = time()
        for audio_thread in streams_to_stop:
            audio_thread.join(
                timeout=(
                    max(0, timeout - (time() - wait_start))
                    if timeout is not None
                    else None
                )
            )

        # Remove references
        for audio_thread in streams_to_stop:
            self._stream_manager.unregister(audio_thread)

    def pause_audio(
        self,
        device: Optional[DeviceType] = None,
        type: Optional[StreamType] = None,  # pylint: disable=redefined-builtin
        streams: Optional[Iterable[Union[str, int]]] = None,
    ):
        """
        Pauses/resumes audio sessions.

        :param device: Filter by host audio device.
        :param type: Filter by stream type (input or output).
        :param streams: Filter by stream indices/names.
        """
        streams_to_pause = self._stream_manager.get(device, type, streams=streams)

        # Send the pause toggle signals
        for audio_thread in streams_to_pause:
            audio_thread.notify_pause()

    def set_volume(
        self,
        volume: float,
        device: Optional[DeviceType] = None,
        streams: Optional[Iterable[Union[str, int]]] = None,
    ):
        """
        :param volume: New volume, between 0 and 100.
        :param device: Set the volume only on the specified device (default:
            all).
        :param streams: Set the volume only on the specified list of stream
            indices/names (default: all).
        """
        stream_objs = self._stream_manager.get(device=device, streams=streams)

        for stream in stream_objs:
            stream.volume = volume
