import shutil
from datetime import datetime, timedelta
from enum import Enum
from threading import Thread, Barrier
from typing import Callable, List

import schedule
from dejavu import Dejavu
from dejavu.recognize import MicrophoneRecognizer

from open_wide.logger import create_logger


class Sound(Enum):
    UPPER = 1
    LOWER = 2


_listen_seconds = 2


class _Recognizer:
    def __init__(self, index: int, callback: Callable[[Sound, int], None]):
        self._logger = create_logger(f"Recognizer{index}")
        self._barrier = Barrier(2)
        self._index = index
        self._callback = callback
        self._stop = False
        Thread(target=self._run).start()

    def _run(self):
        self._dejavu = Dejavu(dburl=f"sqlite:///bells{self._index}.sqlite")
        while not self._stop:
            self._barrier.wait()
            self._recognize()

    def recognize(self):
        self._barrier.wait()

    def _recognize(self):
        song = self._dejavu.recognize(MicrophoneRecognizer, seconds=_listen_seconds, channels=1)
        if song:
            sound = Sound(song['song_id'])
            confidence = song['confidence']
            self._callback(sound, confidence)

    def close(self):
        self._stop = True
        self._barrier.abort()


class _Hook:
    def __init__(self, confidence: int, cooldown: timedelta, callback: Callable[[Sound], None]):
        self._confidence = confidence
        self._cooldown = cooldown
        self._last_call: datetime = datetime.min
        self._callback = callback

    def on_recognized(self, sound: Sound, confidence: int):
        if confidence >= self._confidence:
            now = datetime.now()
            if now >= self._last_call + self._cooldown:
                self._last_call = now
                self._callback(sound)


class Listener:
    def __init__(self):
        self._logger = create_logger("Listener")
        self._hooks: List[_Hook] = []
        self._listener_index = 0
        self._stop = False
        self._recognizers: List[_Recognizer] = []
        for index in range(3):
            self._init_recognizer(index)
        schedule.every(_listen_seconds - 1).seconds.do(self._listen)
        Thread(target=self._run_scheduled).start()

    def _init_recognizer(self, index: int):
        shutil.copy("bells.sqlite", f"bells{index}.sqlite")
        self._recognizers.insert(index, _Recognizer(index, self._on_recognized))

    def _get_listener_index(self) -> int:
        result = self._listener_index
        self._listener_index = (self._listener_index + 1) % 3
        return result

    def _run_scheduled(self):
        while not self._stop:
            schedule.run_pending()

    def _listen(self):
        index = self._get_listener_index()
        recognizer = self._recognizers[index]
        recognizer.recognize()

    def _on_recognized(self, sound: Sound, confidence: int):
        self._logger.debug(f"Recognized sound {sound.name} with confidence {confidence}")
        for hook in self._hooks:
            hook.on_recognized(sound, confidence)

    def register_hook(self,
                      hook: Callable[[Sound], None],
                      confidence: int = 8,
                      cooldown: timedelta = timedelta(seconds=8)):
        """
        Registers a hook to be called if a sound is detected.
        :param hook: the callable that is called whenever a sound is detected
        :param confidence: the minimum confidence a detected sound should have.
        The range is a mystery as of writing this docs.
        :param cooldown: the cooldown time between calls to this hook
        """
        self._hooks.append(_Hook(confidence, cooldown, hook))

    def close(self):
        for recognizer in self._recognizers:
            recognizer.close()
        self._stop = True
        self._hooks.clear()
