"""
A fully functional, multi-channel sound system.
"""
from __future__ import annotations
import ctypes
from os import path, walk
from typing import Dict
import sdl2.sdlmixer as mixer
from sdl2 import AUDIO_F32

from . import IdError, get_path

if mixer.Mix_OpenAudio(48000, AUDIO_F32, 2, 2048):
    raise Exception("Could not open audio device.")


@ctypes.CFUNCTYPE(None, ctypes.c_int)
def channel_finish_callback(channel_num: int):
    sound = Sound.active_channels.pop(channel_num)
    sound.channels &= ~(2**channel_num)


mixer.Mix_ChannelFinished(channel_finish_callback)


class Sound():
    """
    A sound class that is used to manage the sounds throughout the project. We support the following audio formats:
        * MP3
        * WAV
        * OGG
        * FLAC
        * MOD
        * MIDI (not always available on linux)
        * OPUS
        * AIFF
        * VOC

    Args:
        rel_path: The relative path to the sound file you wish to import.
        sound_name: The name of the sound. Defaults to the name of the file.

    Attributes:
        loaded_sounds (Dict[str, Sound]): A dictionary housing all the loaded
            sounds, stored by their name.
    """
    STOPPED = 0
    PLAYING = 1
    PAUSED = 2

    loaded_sounds: Dict[str, Sound] = {}
    active_channels: Dict[int, Sound] = {}

    def __init__(self, rel_path: str, sound_name: str = None):
        self.chunk = mixer.Mix_LoadWAV(rel_path.encode("utf-8"))
        self.channels = 0
        self._paused = False

        if not sound_name:
            self.name = rel_path.split("/")[-1].split(".")[0]
        else:
            self.name = sound_name

        if self.name in Sound.loaded_sounds:
            mixer.Mix_FreeChunk(self.chunk)
            raise IdError(f"There is already a sound with the name {self.name}")
        else:
            Sound.loaded_sounds[self.name] = self

    @property
    def state(self) -> int:
        """
        The current state of the sound.

        The possible states are::

            Sound.STOPPED
            Sound.PLAYING
            Sound.PAUSED

        Returns:
            int: The current state of the sound.
        """
        if not self.channels:
            return self.STOPPED
        elif self._paused:
            return self.PAUSED
        else:
            return self.PLAYING

    def play(self, loops: int = 0):
        """
        Plays a sound.

        Args:
            sound_name: The name of the sound to play.
            loops: The number of times to loop a sound after the first play
                through. Use -1 to loop forever. Defaults to 0.
        """
        channel: int = mixer.Mix_PlayChannel(-1, self.chunk, loops)

        if channel == -1:
            mixer.Mix_AllocateChannels(mixer.Mix_AllocateChannels(-1) + 1)
            channel: int = mixer.Mix_PlayChannel(-1, self.chunk, loops)

        if self._paused:
            mixer.Mix_Pause(channel)

        Sound.active_channels[channel] = self
        self.channels |= 2**channel

    def stop(self):
        """
        Stops all instances of the sound.
        """
        for i in range(self.channels.bit_count()):
            if self.channels & (1 << i):
                mixer.Mix_HaltChannel(i)
                if not self.channels:
                    return

    def pause(self):
        """
        Pauses all instances of the sound.
        """
        for i in range(self.channels.bit_count()):
            if self.channels & (1 << i):
                mixer.Mix_Pause(i)
        self._paused = True

    def resume(self):
        """
        Resumes all instance of the sound.
        """
        for i in range(self.channels.bit_count()):
            if self.channels & (1 << i):
                mixer.Mix_Resume(i)
        self._paused = False

    @classmethod
    def import_sound_folder(cls, rel_path: str, duplicate_names=False, recursive: bool = True):
        """
        Imports a folder of sounds, saving each one in the loaded_sounds
        dictionary by filename.

        Args:
            rel_path: The relative path to the folder you wish to import.
            duplicate_names: if you wish to have duplicate names to your sounds,
            it will use the relative and the sound path for the sounds name
            recursive: Whether it will import an animation shallowly or recursively. Defaults to True.
        """
        p = get_path(rel_path)

        if not recursive:
            _, _, files = next(walk(p))
            # walk to directory path and ignore name and subdirectories
            for sound_path in files:
                path_to_sound = path.join(p, sound_path)
                name = (p + sound_path).split(".")[0] if duplicate_names else sound_path.split(".")[0]
                try:
                    cls(path_to_sound, name)
                except IdError as err:
                    raise Warning("If you have files with duplicate names you must set duplicate_names"
                                  "to True") from err
        else:
            for _, _, files in walk(p):
                # walk to directory path and ignore name and subdirectories
                for sound_path in files:
                    path_to_sound = path.join(p, sound_path)
                    name = (rel_path + "/" + sound_path).split(".")[0] if duplicate_names else sound_path.split(".")[0]
                    try:
                        cls(path_to_sound, name)
                    except IdError as err:
                        raise Warning("If you have files with duplicate names you must set duplicate_names"
                                      "to True") from err

    @classmethod
    def get_sound(cls, sound_name: str) -> Sound:
        """
        Gets the sound based on the sound name.

        Args:
            sound_name: The name of the sound.

        Raises:
            IdError: No sound is associated to the sound name.

        Returns:
            Sound: The sound.
        """
        try:
            return cls.loaded_sounds[sound_name]
        except KeyError as e:
            raise IdError(f"No sound with the name {sound_name} found") from e
